├── requirements.txt ├── setup.cfg ├── Makefile ├── AccountDetails.py.example ├── requirements-dev.txt ├── .travis.yml ├── CONTRIBUTORS.md ├── .gitignore ├── LICENSE ├── tests ├── convert_result.html ├── test_converter.py └── test_export_saved.py ├── README.md.tmpl ├── README.md └── export_saved.py /requirements.txt: -------------------------------------------------------------------------------- 1 | praw==4.4.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | ignore = 4 | D400, 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: readme 2 | 3 | readme: 4 | python -m cogapp -d ./README.md.tmpl > ./README.md -------------------------------------------------------------------------------- /AccountDetails.py.example: -------------------------------------------------------------------------------- 1 | REDDIT_USERNAME = '' 2 | REDDIT_PASSWORD = '' 3 | CLIENT_ID = '' 4 | CLIENT_SECRET = '' 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==3.0.6 3 | pytest-cov==2.4.0 4 | flake8==3.3.0 5 | cogapp==2.5.1 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.5" 5 | install: 6 | - pip3 install -r ./requirements-dev.txt 7 | script: 8 | - "python -m flake8 ./export_saved.py" 9 | - "python -m pytest -v ./tests/ --cov=export_saved" 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # `Export Saved Reddit` Contributors 2 | * [csu][1] 3 | * [rachmadaniHaryono][2] 4 | * [MoHD20][3] 5 | * [favrik][4] 6 | * [DamienRobert][5] 7 | * [kevinwaddle][6] 8 | 9 | [1]: https://github.com/csu 10 | [2]: https://github.com/rachmadaniHaryono 11 | [3]: https://github.com/MoHD20 12 | [4]: https://github.com/favrik 13 | [5]: https://github.com/DamienRobert 14 | [6]: https://github.com/kevinwaddle 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | .cache/ 3 | .coverage 4 | 5 | # private info 6 | AccountDetails.py 7 | *.html 8 | *.csv 9 | *.json 10 | *.pyc 11 | *.log 12 | 13 | # python 14 | venv 15 | 16 | # osx noise 17 | .DS_Store 18 | profile 19 | 20 | # xcode noise 21 | build/* 22 | *.mode1 23 | *.mode1v3 24 | *.mode2v3 25 | *.perspective 26 | *.perspectivev3 27 | *.pbxuser 28 | *.xcworkspace 29 | xcuserdata 30 | 31 | # svn & cvs 32 | .svn 33 | CVS -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Christopher Su 2 | 3 | 4 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 10 | SOFTWARE. -------------------------------------------------------------------------------- /tests/convert_result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
6 |
8 |
10 |
13 |
15 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /tests/test_converter.py: -------------------------------------------------------------------------------- 1 | """test class.""" 2 | import os 3 | from unittest import mock 4 | 5 | import pytest 6 | 7 | 8 | def test_init(): 9 | """test init.""" 10 | file_ = mock.Mock() 11 | from export_saved import Converter 12 | obj = Converter(file=file_) 13 | assert obj._file == file_ 14 | 15 | 16 | def test_init_optional_parameters(): 17 | """test init.""" 18 | file_ = mock.Mock() 19 | html_file_ = mock.Mock() 20 | folder_name_ = mock.Mock() 21 | from export_saved import Converter 22 | obj = Converter(file=file_, html_file=html_file_, folder_name=folder_name_) 23 | assert obj._file == file_ 24 | assert obj._html_file == html_file_ 25 | assert obj._folder_name == folder_name_ 26 | 27 | 28 | @pytest.mark.parametrize( 29 | 'urls_lists, exp_res', 30 | [ 31 | ( 32 | [ 33 | ['header0', 'header1', 'header2', 'header3', 'header4'], 34 | ['url0', 'title0', '0', None, 'folder1'], 35 | ['url1', 'title1', '1', None, 'folder2'], 36 | ['url2', 'title2', '2', None, 'folder1'], 37 | [], 38 | ], 39 | { 40 | 'folder1': [ 41 | ['url0', 'title0', '0'], 42 | ['url2', 'title2', '2'], 43 | ], 44 | 'folder2': [ 45 | ['url1', 'title1', '1'] 46 | ] 47 | } 48 | ) 49 | ] 50 | ) 51 | def test_parse_url(urls_lists, exp_res): 52 | """test method.""" 53 | with mock.patch('export_saved.open'), \ 54 | mock.patch('export_saved.csv') as m_csv: 55 | m_csv.reader.return_value = iter(urls_lists) 56 | from export_saved import Converter 57 | obj = Converter(file=mock.Mock()) 58 | res = obj.parse_urls() 59 | assert res == exp_res 60 | 61 | 62 | def test_convert(): 63 | """test method.""" 64 | parse_urls_result = { 65 | 'folder1': [ 66 | ['url0', 'title0', '0'], 67 | ['url2', 'title2', '2'], 68 | ], 69 | 'folder2': [ 70 | ['url1', 'title1', '1'] 71 | ] 72 | } 73 | ifile = mock.Mock() 74 | exp_bookmark_html = os.path.join(os.path.dirname(__file__), 'convert_result.html') 75 | with open(exp_bookmark_html) as ff: 76 | exp_bookmark_html_text = ff.read() 77 | with mock.patch('export_saved.open') as m_open, \ 78 | mock.patch('export_saved.time', return_value=0): 79 | m_open.return_value = ifile 80 | from export_saved import Converter 81 | obj = Converter(file=mock.Mock()) 82 | obj.parse_urls = mock.Mock(return_value=parse_urls_result) 83 | obj.convert() 84 | content = ifile.write.call_args[0][0] 85 | for exp_line, c_line in zip(exp_bookmark_html_text.splitlines(), content.splitlines()): 86 | assert exp_line == c_line 87 | -------------------------------------------------------------------------------- /README.md.tmpl: -------------------------------------------------------------------------------- 1 | # Export Saved Reddit Posts 2 | 3 | [](https://travis-ci.org/csu/export-saved-reddit) [](https://codecov.io/gh/csu/export-saved-reddit) 4 | 5 | Exports saved and/or upvoted Reddit posts into a HTML file that is ready to be imported into Google Chrome. Sorts items into folders by subreddit. 6 | 7 | ## Requirements 8 | * [Python 3.x](https://www.python.org/downloads/) 9 | * [pip](https://pip.pypa.io/en/stable/installing/) 10 | * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) (recommended) 11 | 12 | ## Installation 13 | First, make sure you have [Python 3.x](https://www.python.org/downloads/), [pip](https://pip.pypa.io/en/stable/installing/), and [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your machine. 14 | 15 | Run the following in your command prompt to install: 16 | 17 | git clone https://github.com/csu/export-saved-reddit.git 18 | cd export-saved-reddit 19 | pip install -r requirements.txt 20 | 21 | To install without git, [download the source code from GitHub](https://github.com/csu/export-saved-reddit/archive/master.zip), extract the archive, and follow the steps above beginning from the second line. 22 | 23 | ## Usage 24 | 1. [Make a new Reddit](https://www.reddit.com/prefs/apps) app to get a `client id` and a `client secret`. 25 | 26 | - Scroll to the bottom of the page and click "create app" 27 | - You can name the app anything (e.g. "export-saved"). Select the "script" option. Put anything for the redirect URI (e.g. https://christopher.su). 28 | - After creating the app, the client id will appear under the app name while the client secret will be labeled "secret". 29 | 30 |  31 | 32 | 2. In the `export-saved-reddit` folder, rename the `AccountDetails.py.example` file to `AccountDetails.py`. 33 | 3. Open the `AccountDetails.py` in a text editor and enter your Reddit username, password, client id, client secret within the corresponding quotation marks. Save and close the file. 34 | 4. Back in your shell, run `python export_saved.py` in the `export-saved-reddit` folder. This will run the export, which will create `chrome-bookmarks.html` and `export-saved.csv` files containing your data in the same folder. 35 | 36 | ### Additional Options 37 | [[[cog 38 | import subprocess 39 | import cogapp as cog 40 | 41 | def indent(text, width=4): 42 | return "\n".join((" "*width + line) for line in text.splitlines()) 43 | 44 | text = subprocess.check_output("python ./export_saved.py --help", shell=True) 45 | cog_obj = cog.Cog() 46 | cog_obj.prout(indent(text.decode('utf8'))) 47 | ]]] 48 | [[[end]]] 49 | 50 | ## Updating 51 | To update the script to the latest version, enter the `export-saved-reddit` folder in your shell/command prompt and enter the following: 52 | 53 | git pull 54 | 55 | ## Help 56 | If you have any questions or comments, please [open an issue on GitHub](https://github.com/csu/export-saved-reddit/issues). 57 | 58 | ## [Contributing](https://github.com/csu/export-saved-reddit/blob/master/CONTRIBUTORS.md) 59 | 60 | If you would like to contribute, check out the project's [open issues](https://github.com/csu/export-saved-reddit/issues). [Pull requests](https://github.com/csu/export-saved-reddit/pulls) are welcome. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Export Saved Reddit Posts 2 | 3 | [](https://travis-ci.org/csu/export-saved-reddit) [](https://codecov.io/gh/csu/export-saved-reddit) 4 | 5 | Exports saved and/or upvoted Reddit posts into a HTML file that is ready to be imported into Google Chrome. Sorts items into folders by subreddit. 6 | 7 | ## Requirements 8 | * [Python 3.x](https://www.python.org/downloads/) 9 | * [pip](https://pip.pypa.io/en/stable/installing/) 10 | * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) (recommended) 11 | 12 | ## Installation 13 | First, make sure you have [Python 3.x](https://www.python.org/downloads/), [pip](https://pip.pypa.io/en/stable/installing/), and [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your machine. 14 | 15 | Run the following in your command prompt to install: 16 | 17 | git clone https://github.com/csu/export-saved-reddit.git 18 | cd export-saved-reddit 19 | pip install -r requirements.txt 20 | 21 | To install without git, [download the source code from GitHub](https://github.com/csu/export-saved-reddit/archive/master.zip), extract the archive, and follow the steps above beginning from the second line. 22 | 23 | ## Usage 24 | 1. [Make a new Reddit](https://www.reddit.com/prefs/apps) app to get a `client id` and a `client secret`. 25 | 26 | - Scroll to the bottom of the page and click "create app" 27 | - You can name the app anything (e.g. "export-saved"). Select the "script" option. Put anything for the redirect URI (e.g. https://christopher.su). 28 | - After creating the app, the client id will appear under the app name while the client secret will be labeled "secret". 29 | 30 |  31 | 32 | 2. In the `export-saved-reddit` folder, rename the `AccountDetails.py.example` file to `AccountDetails.py`. 33 | 3. Open the `AccountDetails.py` in a text editor and enter your Reddit username, password, client id, client secret within the corresponding quotation marks. Save and close the file. 34 | 4. Back in your shell, run `python export_saved.py` in the `export-saved-reddit` folder. This will run the export, which will create `chrome-bookmarks.html` and `export-saved.csv` files containing your data in the same folder. 35 | 36 | ### Additional Options 37 | usage: export_saved.py [-h] [-u USERNAME] [-p PASSWORD] [-id CLIENT_ID] 38 | [-s CLIENT_SECRET] [-v] [-up] [-all] [-V] 39 | 40 | Exports saved Reddit posts into a HTML file that is ready to be imported into 41 | Google Chrome or Firefox 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | -u USERNAME, --username USERNAME 46 | pass in username as argument 47 | -p PASSWORD, --password PASSWORD 48 | pass in password as argument 49 | -id CLIENT_ID, --client-id CLIENT_ID 50 | pass in client id as argument 51 | -s CLIENT_SECRET, --client-secret CLIENT_SECRET 52 | pass in client secret as argument 53 | -v, --verbose increase output verbosity (deprecated; doesn't do 54 | anything now) 55 | -up, --upvoted get upvoted posts instead of saved posts 56 | -all, --all get upvoted, saved, comments and submissions 57 | -V, --version get program version. 58 | 59 | ## Updating 60 | To update the script to the latest version, enter the `export-saved-reddit` folder in your shell/command prompt and enter the following: 61 | 62 | git pull 63 | 64 | ## Help 65 | If you have any questions or comments, please [open an issue on GitHub](https://github.com/csu/export-saved-reddit/issues). 66 | 67 | ## [Contributing](https://github.com/csu/export-saved-reddit/blob/master/CONTRIBUTORS.md) 68 | 69 | If you would like to contribute, check out the project's [open issues](https://github.com/csu/export-saved-reddit/issues). [Pull requests](https://github.com/csu/export-saved-reddit/pulls) are welcome. 70 | -------------------------------------------------------------------------------- /tests/test_export_saved.py: -------------------------------------------------------------------------------- 1 | """test module.""" 2 | from itertools import product 3 | from unittest import mock 4 | import argparse 5 | import os 6 | 7 | import pytest 8 | 9 | 10 | @pytest.mark.parametrize( 11 | 'argv, exp_res', 12 | [( 13 | [], 14 | { 15 | 'all': False, 'upvoted': False, 'verbose': False, 16 | 'client_secret': None, 'password': None, 17 | 'username': None, 'client_id': None, 18 | 'version': False 19 | } 20 | )] 21 | ) 22 | def test_get_args(argv, exp_res): 23 | """test func.""" 24 | from export_saved import get_args 25 | res = get_args(argv) 26 | assert res.__dict__ == exp_res 27 | 28 | 29 | def get_str_permalink(): 30 | """Return string permalink.""" 31 | return 'permalink' 32 | 33 | 34 | @pytest.mark.parametrize( 35 | 'item, csv_rows', 36 | [ 37 | ( 38 | argparse.Namespace( 39 | link_title='link_title', subreddit='subreddit', 40 | permalink='permalink', created='10'), 41 | ['https://www.reddit.com/permalink', 'link_title', 10, None, 'subreddit'] 42 | ), 43 | ( 44 | argparse.Namespace( 45 | title='title', permalink='permalink', created='invalid'), 46 | ['https://www.reddit.com/permalink', 'title', 0, None, 'None'] 47 | ), 48 | ( 49 | argparse.Namespace( 50 | title='title', permalink=get_str_permalink, created='invalid'), 51 | ['https://www.reddit.com/permalink', 'title', 0, None, 'None'] 52 | ), 53 | ] 54 | ) 55 | def test_get_csv_rows(item, csv_rows): 56 | """test func.""" 57 | from export_saved import get_csv_rows 58 | reddit = mock.Mock() 59 | reddit.config.reddit_url = "https://www.reddit.com/" 60 | res = get_csv_rows(reddit, seq=[item]) 61 | assert res == [csv_rows] 62 | 63 | 64 | def test_write_csv_with_str(tmpdir): 65 | """test func.""" 66 | file_name = os.path.join(tmpdir.strpath, 'test.csv') 67 | csv_rows = ['url1', 'title1', '10', '', 'folder1'] 68 | from export_saved import write_csv 69 | write_csv(csv_rows, file_name) 70 | 71 | 72 | def test_write_csv(): 73 | """test func.""" 74 | csv_rows = [['url1', 'title1', '10', '', 'folder1']] 75 | with mock.patch('export_saved.open') as m_open: 76 | from export_saved import write_csv 77 | write_csv(csv_rows) 78 | m_open.return_value.__enter__.return_value.assert_has_calls([ 79 | mock.call.write('URL,Title,Created,Selection,Folder\r\n'), 80 | mock.call.write('url1,title1,10,,folder1\r\n') 81 | ]) 82 | 83 | 84 | def test_login(): 85 | """test func.""" 86 | args = mock.Mock() 87 | account = { 88 | 'username': mock.Mock(), 89 | 'password': mock.Mock(), 90 | 'client_id': mock.Mock(), 91 | 'client_secret': mock.Mock(), 92 | } 93 | with mock.patch('export_saved.praw') as m_praw, \ 94 | mock.patch('export_saved.account_details') as m_ad: 95 | m_ad.return_value = account 96 | from export_saved import login 97 | res = login(args) 98 | m_ad.assert_called_once_with(args=args) 99 | assert res == m_praw.Reddit.return_value 100 | 101 | 102 | @pytest.mark.parametrize( 103 | 'username, password, client_id, client_secret', 104 | product([None, mock.Mock()], repeat=4) 105 | ) 106 | def test_account_details(username, password, client_id, client_secret): 107 | """test account details.""" 108 | ns_kwargs = { 109 | 'username': username, 110 | 'password': password, 111 | 'client_id': client_id, 112 | 'client_secret': client_secret, 113 | } 114 | args = argparse.Namespace(**ns_kwargs) 115 | cond_match = all(ns_kwargs[key] for key in ns_kwargs) 116 | from export_saved import account_details 117 | if not cond_match: 118 | with pytest.raises(SystemExit): 119 | with mock.patch.dict('sys.modules', {'AccountDetails': None}): 120 | account_details(args) 121 | return 122 | res = account_details(args) 123 | assert res == ns_kwargs 124 | 125 | 126 | def test_process(): 127 | """test func.""" 128 | reddit = mock.Mock() 129 | seq = mock.Mock() 130 | file_name = 'filename' 131 | folder_name = 'folder_name' 132 | with mock.patch('export_saved.get_csv_rows'),\ 133 | mock.patch('export_saved.write_csv'),\ 134 | mock.patch('export_saved.Converter') as m_converter: 135 | from export_saved import process 136 | # run 137 | process(reddit, seq, file_name, folder_name) 138 | # test 139 | m_converter.assert_has_calls([ 140 | mock.call('filename.csv', 'filename.html', folder_name=folder_name), 141 | mock.call().convert() 142 | ]) 143 | 144 | 145 | @pytest.mark.parametrize('key', ['upvoted', 'saved', 'comments', 'submissions']) 146 | def test_save(key): 147 | """test func.""" 148 | reddit = mock.Mock() 149 | seq = mock.Mock() 150 | if key in ('saved', 'upvoted'): 151 | getattr(reddit.user.me.return_value, key).return_value = seq 152 | else: 153 | getattr(reddit.user.me.return_value, key).new.return_value = seq 154 | with mock.patch('export_saved.process') as m_p: 155 | import export_saved 156 | getattr(export_saved, 'save_{}'.format(key))(reddit) 157 | m_p.assert_called_once_with( 158 | reddit, seq, 'export-{}'.format(key), "Reddit - {}".format(key.title())) 159 | 160 | 161 | @pytest.mark.parametrize('verbose, upvoted, all', product([True, False], repeat=3)) 162 | def test_main(verbose, upvoted, all): 163 | """test func""" 164 | reddit = mock.Mock() 165 | with mock.patch('export_saved.get_args') as m_ga, \ 166 | mock.patch('export_saved.logging') as m_logging, \ 167 | mock.patch('export_saved.login') as m_login, \ 168 | mock.patch('export_saved.save_upvoted'), \ 169 | mock.patch('export_saved.save_saved'), \ 170 | mock.patch('export_saved.save_submissions'), \ 171 | mock.patch('export_saved.save_comments'): 172 | m_login.return_value = reddit 173 | m_ga.return_value = argparse.Namespace( 174 | verbose=verbose, 175 | upvoted=upvoted, 176 | all=all, 177 | version=False 178 | ) 179 | import export_saved 180 | # run 181 | with pytest.raises(SystemExit): 182 | export_saved.main() 183 | if verbose: 184 | m_logging.basicConfig.assert_called_once_with(level=m_logging.DEBUG) 185 | if upvoted: 186 | export_saved.save_upvoted.assert_called_once_with(reddit) 187 | elif all: 188 | export_saved.save_upvoted.assert_called_once_with(reddit) 189 | export_saved.save_saved.assert_called_once_with(reddit) 190 | export_saved.save_submissions.assert_called_once_with(reddit) 191 | export_saved.save_comments.assert_called_once_with(reddit) 192 | else: 193 | export_saved.save_saved.assert_called_once_with(reddit) 194 | -------------------------------------------------------------------------------- /export_saved.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | export-saved.py 4 | 5 | Christopher Su 6 | Exports saved Reddit posts into a HTML file that is ready to be imported into Google Chrome. 7 | """ 8 | 9 | from time import time 10 | import argparse 11 | import csv 12 | import logging 13 | import sys 14 | 15 | import praw 16 | 17 | 18 | __version__ = '0.1.2' 19 | 20 | 21 | # # Converter class from https://gist.github.com/raphaa/1327761 22 | class Converter(): 23 | """Converts a CSV instapaper export to a Chrome bookmark file.""" 24 | 25 | def __init__(self, file, html_file=None, folder_name="Reddit"): 26 | """init method.""" 27 | self._file = file 28 | self._html_file = html_file if html_file is not None else 'chrome-bookmarks.html' 29 | self._folder_name = folder_name if folder_name is not None else 'Reddit' 30 | 31 | def parse_urls(self): 32 | """Parse the file and returns a folder ordered list.""" 33 | efile = open(self._file) 34 | urls = csv.reader(efile, dialect='excel') 35 | parsed_urls = {} 36 | next(urls) 37 | for url in urls: 38 | if not url: 39 | continue 40 | else: 41 | folder = url[4].strip() 42 | if folder not in list(parsed_urls.keys()): 43 | parsed_urls[folder] = [] 44 | parsed_urls[folder].append([url[0], url[1], url[2]]) 45 | return parsed_urls 46 | 47 | def convert(self): 48 | """Convert the file.""" 49 | urls = self.parse_urls() 50 | t = int(time()) 51 | content = ('\n' 52 | '\n
\n
\n' % {'t': t, 'folder_name': self._folder_name}) 57 | 58 | for folder in sorted(list(urls.keys())): 59 | content += ('
\n' % {'t': t, 'n': folder}) 61 | for url, title, add_date in urls[folder]: 62 | content += ('
\n' 66 | content += '
\n' * 3 67 | ifile = open(self._html_file, 'wb') 68 | try: 69 | ifile.write(content) 70 | except TypeError: 71 | ifile.write(content.encode('utf-8', 'ignore')) 72 | 73 | 74 | def get_args(argv): 75 | """get args. 76 | 77 | Args: 78 | argv (list): List of arguments. 79 | 80 | Returns: 81 | argparse.Namespace: Parsed arguments. 82 | """ 83 | parser = argparse.ArgumentParser( 84 | description=( 85 | 'Exports saved Reddit posts into a HTML file ' 86 | 'that is ready to be imported into Google Chrome or Firefox' 87 | ) 88 | ) 89 | 90 | parser.add_argument("-u", "--username", help="pass in username as argument") 91 | parser.add_argument("-p", "--password", help="pass in password as argument") 92 | parser.add_argument("-id", "--client-id", help="pass in client id as argument") 93 | parser.add_argument("-s", "--client-secret", help="pass in client secret as argument") 94 | 95 | parser.add_argument("-v", "--verbose", 96 | help="increase output verbosity (deprecated; doesn't do anything now)", 97 | action="store_true") 98 | parser.add_argument("-up", "--upvoted", 99 | help="get upvoted posts instead of saved posts", 100 | action="store_true") 101 | parser.add_argument("-all", "--all", 102 | help="get upvoted, saved, comments and submissions", 103 | action="store_true") 104 | parser.add_argument("-V", "--version", 105 | help="get program version.", 106 | action="store_true") 107 | 108 | args = parser.parse_args(argv) 109 | return args 110 | 111 | 112 | def login(args): 113 | """login method. 114 | 115 | Args: 116 | args (argparse.Namespace): Parsed arguments. 117 | 118 | Returns: a logged on praw instance 119 | """ 120 | # login 121 | account = account_details(args=args) 122 | client_id = account['client_id'] 123 | client_secret = account['client_secret'] 124 | username = account['username'] 125 | password = account['password'] 126 | reddit = praw.Reddit(client_id=client_id, 127 | client_secret=client_secret, 128 | user_agent='export saved 2.0', 129 | username=username, 130 | password=password) 131 | logging.info('Login succesful') 132 | return reddit 133 | 134 | 135 | def account_details(args): 136 | """Extract account informations. 137 | 138 | Args: 139 | args (argparse.Namespace): Parsed arguments. 140 | 141 | Returns: 142 | Account details 143 | """ 144 | username = None 145 | password = None 146 | client_id = None 147 | client_secret = None 148 | if args and args.username and args.password and args.client_id and args.client_secret: 149 | username = args.username 150 | password = args.password 151 | client_id = args.client_id 152 | client_secret = args.client_secret 153 | else: 154 | try: # pragma: no cover 155 | import AccountDetails 156 | username = AccountDetails.REDDIT_USERNAME 157 | password = AccountDetails.REDDIT_PASSWORD 158 | client_id = AccountDetails.CLIENT_ID 159 | client_secret = AccountDetails.CLIENT_SECRET 160 | except ImportError: 161 | print( 162 | 'You must specify a username, password, client id, ' 163 | 'and client secret, either in an AccountDetails file ' 164 | 'or by passing them as arguments (run the script with ' 165 | 'the --help flag for more info).' 166 | ) 167 | exit(1) 168 | 169 | if not username or not password or not client_id or not client_secret: # pragma: no cover 170 | print('You must specify ALL the arguments') 171 | print( 172 | 'Either use the options (write [-h] for help menu) ' 173 | 'or add them to an AccountDetails module.' 174 | ) 175 | exit(1) 176 | return { 177 | 'username': username, 178 | 'password': password, 179 | 'client_id': client_id, 180 | 'client_secret': client_secret, 181 | } 182 | 183 | 184 | def get_csv_rows(reddit, seq): 185 | """get csv rows. 186 | 187 | Args: 188 | reddit: reddit praw's instance 189 | seq (list): List of Reddit item. 190 | 191 | Returns: 192 | list: Parsed reddit item. 193 | """ 194 | csv_rows = [] 195 | reddit_url = reddit.config.reddit_url 196 | 197 | # filter items for link 198 | for idx, i in enumerate(seq, 1): 199 | logging.info('processing item #{}'.format(idx)) 200 | 201 | if not hasattr(i, 'title'): 202 | i.title = i.link_title 203 | 204 | # Fix possible buggy utf-8 205 | title = i.title.encode('utf-8').decode('utf-8') 206 | try: 207 | logging.info('title: {}'.format(title)) 208 | except UnicodeEncodeError: 209 | logging.info('title: {}'.format(title.encode('utf8', 'ignore'))) 210 | 211 | try: 212 | created = int(i.created) 213 | except ValueError: 214 | created = 0 215 | 216 | try: 217 | folder = str(i.subreddit).encode('utf-8').decode('utf-8') 218 | except AttributeError: 219 | folder = "None" 220 | 221 | if callable(i.permalink): 222 | permalink = i.permalink() 223 | else: 224 | permalink = i.permalink 225 | permalink = permalink.encode('utf-8').decode('utf-8') 226 | 227 | csv_rows.append([reddit_url + permalink, title, created, None, folder]) 228 | 229 | return csv_rows 230 | 231 | 232 | def write_csv(csv_rows, file_name=None): 233 | """write csv using csv module. 234 | 235 | Args: 236 | csv_rows (list): CSV rows. 237 | file_name (string): filename written 238 | """ 239 | file_name = file_name if file_name is not None else 'export-saved.csv' 240 | 241 | # csv setting 242 | csv_fields = ['URL', 'Title', 'Created', 'Selection', 'Folder'] 243 | delimiter = ',' 244 | 245 | # write csv using csv module 246 | try: 247 | with open(file_name, "wb") as f: 248 | csvwriter = csv.writer(f, delimiter=delimiter, quoting=csv.QUOTE_MINIMAL) 249 | csvwriter.writerow(csv_fields) 250 | for row in csv_rows: 251 | csvwriter.writerow(row) 252 | except (UnicodeEncodeError, TypeError) as e: 253 | with open(file_name, "w") as f: 254 | csvwriter = csv.writer(f, delimiter=delimiter, quoting=csv.QUOTE_MINIMAL) 255 | csvwriter.writerow(csv_fields) 256 | for row in csv_rows: 257 | try: 258 | csvwriter.writerow(row) 259 | except UnicodeEncodeError: 260 | csvwriter.writerow([r.encode('utf-8', 'ignore') 261 | if isinstance(r, str) else r for r in row]) 262 | 263 | 264 | def process(reddit, seq, file_name, folder_name): 265 | """Write csv and html from a list of results. 266 | 267 | Args: 268 | reddit: reddit praw's instance 269 | seq (list): list to write 270 | file_name: base file name without extension 271 | folder_name: top level folder name for the exported html bookmarks 272 | """ 273 | csv_rows = get_csv_rows(reddit, seq) 274 | # write csv using csv module 275 | write_csv(csv_rows, file_name + ".csv") 276 | logging.info('csv written.') 277 | # convert csv to bookmark 278 | converter = Converter(file_name + ".csv", file_name + ".html", 279 | folder_name=folder_name) 280 | converter.convert() 281 | logging.info('html written.') 282 | 283 | 284 | def save_upvoted(reddit): 285 | """ save upvoted posts """ 286 | seq = reddit.user.me().upvoted(limit=None) 287 | process(reddit, seq, "export-upvoted", "Reddit - Upvoted") 288 | 289 | 290 | def save_saved(reddit): 291 | """ save saved posts """ 292 | seq = reddit.user.me().saved(limit=None) 293 | process(reddit, seq, "export-saved", "Reddit - Saved") 294 | 295 | 296 | def save_comments(reddit): 297 | """ save comments posts """ 298 | seq = reddit.user.me().comments.new(limit=None) 299 | process(reddit, seq, "export-comments", "Reddit - Comments") 300 | 301 | 302 | def save_submissions(reddit): 303 | """ save saved posts """ 304 | seq = reddit.user.me().submissions.new(limit=None) 305 | process(reddit, seq, "export-submissions", "Reddit - Submissions") 306 | 307 | 308 | def main(): 309 | """main func.""" 310 | args = get_args(sys.argv[1:]) 311 | 312 | # set logging config 313 | if args.verbose: 314 | logging.basicConfig(level=logging.DEBUG) 315 | 316 | # print program version. 317 | if args.version: 318 | print(__version__) 319 | return 320 | 321 | reddit = login(args=args) 322 | if args.upvoted: 323 | save_upvoted(reddit) 324 | elif args.all: 325 | save_upvoted(reddit) 326 | save_saved(reddit) 327 | save_submissions(reddit) 328 | save_comments(reddit) 329 | else: 330 | save_saved(reddit) 331 | 332 | sys.exit(0) 333 | 334 | 335 | if __name__ == "__main__": # pragma: no cover 336 | main() 337 | --------------------------------------------------------------------------------