diff --git a/AUTHORS.rst b/AUTHORS.rst index b0ce92a..e9e35f3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,4 +1,3 @@ - ================ RTV Contributors ================ @@ -7,22 +6,26 @@ Thanks to the following people for their contributions to this project. * `Michael Lazar `_ * `Tobin Brown `_ +* `woorst `_ * `Théo Piboubès `_ * `Yusuke Sakamoto `_ * `Johnathan Jenkins `_ -* `obosob `_ +* `Gustavo Zambonin `_ * `mekhami `_ -* `Noah Morrison `_ +* `obosob `_ * `Toby Hughes `_ -* `Shawn Hind `_ +* `Noah Morrison `_ +* `mardiqwop `_ +* `Shawn Hind `_ +* `5225225 `_ * `JuanPablo `_ * `Robert Greener `_ -* `peterpans01 `_ -* `Adam Talsma `_ +* `afloofloo `_ +* `Matthew Smith `_ +* `Marc Abramowitz `_ +* `Hans Roman `_ * `Ram-Z `_ -* `mralext20 `_ -* `Wieland Hoffmann `_ -* `Marc Abramowitz `_ -* `Hans Roman `_ -* `Gustavo Zambonin `_ - +* `Wieland Hoffmann `_ +* `Adam Talsma `_ +* `Alexander Terry `_ +* `peterpans01 `_ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 10eea76..29b4bae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,7 @@ include version.py include CHANGELOG.rst -include CONTRIBUTORS.rst +include AUTHORS.rst include README.rst include LICENSE include rtv.1 -include rtv/rtv.cfg include rtv/templates/* diff --git a/README.rst b/README.rst index 79bb11f..6fa3105 100644 --- a/README.rst +++ b/README.rst @@ -76,14 +76,14 @@ Move the cursor using either the arrow keys or *Vim* style movement Press ``/`` to open the navigation prompt, where you can type things like -- /front -- /r/commandprompt+linuxmasterrace -- /r/programming/controversial-week -- /u/me -- /u/multi-mod/m/art -- /domain/github.com +- ``/front`` +- ``/r/commandprompt+linuxmasterrace`` +- ``/r/programming/controversial-week`` +- ``/u/me`` +- ``/u/multi-mod/m/art`` +- ``/domain/github.com`` -See `CONTROLS `_ for the full list of commands. +See `CONTROLS `_ for the full list of commands ======== Settings @@ -93,55 +93,34 @@ Settings Configuration ------------- -Configuration settings are stored in ``{HOME}/.config/rtv/rtv.cfg``. -Auto-generate the config file by running +Configuration files are stored in the ``{HOME}/.config/rtv/`` directory + +See `rtv.cfg `_ for the full list of configurable options. You can clone this file onto your system by running .. code-block:: bash $ rtv --copy-config -See the `default config `_ for the full list of settings. - ------- -Editor ------- - -You can compose posts and reply to comments using your preferred text editor. -Set the editor by changing ``$RTV_EDITOR`` in your environment. - -.. code-block:: bash - - $ export RTV_EDITOR=gedit - -If not specified, the default system ``$EDITOR`` (or *nano*) will be used. - ----------- -Web Browser +Environment ----------- -You can open submission links using your web browser. -On most systems the default web browser will open in a new window. -If you prefer the complete terminal experience, set ``$BROWSER`` to a console-based web browser. +RTV will respect the following environment variables when accessing external programs -.. code-block:: bash - - $ export BROWSER=w3m - -`w3m `_, `lynx `_, and `elinks `_ are all good choices. - ----------- -Url Viewer ----------- - -You can extract links from inside of comments using urlview. -Use ``$RTV_URLVIEWER`` to specify a custom url viewer. - -.. code-block:: bash - - $ export RTV_URLVIEWER=urlview - -`urlview `_ and `urlscan `_ are known to be compatible, but any program that accepts text via a stdin pipe will do. -These applications don't come pre-installed, but are available through most systems' package managers. +``$BROWSER`` + Submission links can be opened in your web browser. + On most systems the default web browser will open in a new window. + If you prefer the complete terminal experience, try using a console-based web browser + (`w3m `_, `lynx `_, and `elinks `_ are all good choices). +``$PAGER`` + Extra long comments and submissions can be viewed through the system pager. +``$RTV_EDITOR`` + Compose posts and replying to comments is done using your preferred text editor. + If not specified, the default system ``$EDITOR`` (or `nano `_) will be used. +``$RTV_URLVIEWER`` + A url viewer can be used to extract links from inside of comments. + `urlview `_ and `urlscan `_ are known to be compatible. + These applications don't come pre-installed, but are available through most systems' package managers. === FAQ diff --git a/rtv/__main__.py b/rtv/__main__.py index e38d9fa..bb002df 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -5,12 +5,14 @@ import os import sys import locale import logging +import warnings +import six import praw import tornado from . import docs -from .config import Config, copy_default_config +from .config import Config, copy_default_config, copy_default_mailcap from .oauth import OAuthHelper from .terminal import Terminal from .objects import curses_session, Color @@ -34,6 +36,10 @@ def main(): # Squelch SSL warnings logging.captureWarnings(True) + if six.PY3: + # These ones get triggered even when capturing warnings is turned on + warnings.simplefilter('ignore', ResourceWarning) #pylint:disable=E0602 + locale.setlocale(locale.LC_ALL, '') # Set the terminal title @@ -59,6 +65,10 @@ def main(): copy_default_config() return + if config['copy_mailcap']: + copy_default_mailcap() + return + # Load the browsing history from previous sessions config.load_history() @@ -103,7 +113,7 @@ def main(): if not config['monochrome']: Color.init() - term = Terminal(stdscr, config['ascii']) + term = Terminal(stdscr, config) with term.loader('Initializing', catch_exception=False): reddit = praw.Reddit(user_agent=user_agent, decode_html_entities=False, diff --git a/rtv/config.py b/rtv/config.py index 9fb38b6..27010bd 100644 --- a/rtv/config.py +++ b/rtv/config.py @@ -16,9 +16,11 @@ from .objects import KeyMap PACKAGE = os.path.dirname(__file__) HOME = os.path.expanduser('~') TEMPLATE = os.path.join(PACKAGE, 'templates') -DEFAULT_CONFIG = os.path.join(PACKAGE, 'rtv.cfg') +DEFAULT_CONFIG = os.path.join(TEMPLATE, 'rtv.cfg') +DEFAULT_MAILCAP = os.path.join(TEMPLATE, 'mailcap') XDG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) CONFIG = os.path.join(XDG_HOME, 'rtv', 'rtv.cfg') +MAILCAP = os.path.join(HOME, '.mailcap') TOKEN = os.path.join(XDG_HOME, 'rtv', 'refresh-token') HISTORY = os.path.join(XDG_HOME, 'rtv', 'history.log') @@ -59,30 +61,50 @@ def build_parser(): parser.add_argument( '--copy-config', dest='copy_config', action='store_const', const=True, help='Copy the default configuration to {HOME}/.config/rtv/rtv.cfg') + parser.add_argument( + '--copy-mailcap', dest='copy_mailcap', action='store_const', const=True, + help='Copy an example mailcap configuration to {HOME}/.mailcap') + parser.add_argument( + '--enable-media', dest='enable_media', action='store_const', const=True, + help='Open external links using programs defined in the mailcap config') return parser +def copy_default_mailcap(filename=MAILCAP): + """ + Copy the example mailcap configuration to the specified file. + """ + return _copy_settings_file(DEFAULT_MAILCAP, filename, 'mailcap') + + def copy_default_config(filename=CONFIG): """ - Copy the default configuration file to the user's {HOME}/.config/rtv + Copy the default rtv user configuration to the specified file. + """ + return _copy_settings_file(DEFAULT_CONFIG, filename, 'config') + + +def _copy_settings_file(source, destination, name): + """ + Copy a file from the repo to the user's home directory. """ - if os.path.exists(filename): + if os.path.exists(destination): try: ch = six.moves.input( - 'File %s already exists, overwrite? y/[n]):' % filename) + 'File %s already exists, overwrite? y/[n]):' % destination) if ch not in ('Y', 'y'): return except KeyboardInterrupt: return - filepath = os.path.dirname(filename) + filepath = os.path.dirname(destination) if not os.path.exists(filepath): os.makedirs(filepath) - print('Copying default settings to %s' % filename) - shutil.copy(DEFAULT_CONFIG, filename) - os.chmod(filename, 0o664) + print('Copying default %s to %s' % (name, destination)) + shutil.copy(source, destination) + os.chmod(destination, 0o664) class OrderedSet(object): @@ -215,6 +237,7 @@ class Config(object): 'monochrome': partial(config.getboolean, 'rtv'), 'clear_auth': partial(config.getboolean, 'rtv'), 'persistent': partial(config.getboolean, 'rtv'), + 'enable_media': partial(config.getboolean, 'rtv'), 'history_size': partial(config.getint, 'rtv'), 'oauth_redirect_port': partial(config.getint, 'rtv'), 'oauth_scope': lambda x: rtv[x].split(',') @@ -240,4 +263,4 @@ class Config(object): filepath = os.path.dirname(filename) if not os.path.exists(filepath): - os.makedirs(filepath) \ No newline at end of file + os.makedirs(filepath) diff --git a/rtv/exceptions.py b/rtv/exceptions.py index bee8924..fd1432d 100644 --- a/rtv/exceptions.py +++ b/rtv/exceptions.py @@ -40,3 +40,7 @@ class BrowserError(RTVError): class TemporaryFileError(RTVError): "Indicates that an error has occurred and the file should not be deleted" + + +class MailcapEntryNotFound(RTVError): + "A valid mailcap entry could not be coerced from the given url" \ No newline at end of file diff --git a/rtv/mime_parsers.py b/rtv/mime_parsers.py new file mode 100644 index 0000000..a33b33c --- /dev/null +++ b/rtv/mime_parsers.py @@ -0,0 +1,175 @@ +import re +import logging +import mimetypes + +import requests +from bs4 import BeautifulSoup + +_logger = logging.getLogger(__name__) + + +class BaseMIMEParser(object): + """ + BaseMIMEParser can be sub-classed to define custom handlers for determining + the MIME type of external urls. + """ + pattern = re.compile(r'.*$') + + @staticmethod + def get_mimetype(url): + """ + Guess based on the file extension. + + Args: + url (text): Web url that was linked to by a reddit submission. + + Returns: + modified_url (text): The url (or filename) that will be used when + constructing the command to run. + content_type (text): The mime-type that will be used when + constructing the command to run. If the mime-type is unknown, + return None and the program will fallback to using the web + browser. + """ + filename = url.split('?')[0] + content_type, _ = mimetypes.guess_type(filename) + return url, content_type + + +class GfycatMIMEParser(BaseMIMEParser): + """ + Gfycat provides a primitive json api to generate image links. URLs can be + downloaded as either gif, webm, or mjpg. Webm was selected because it's + fast and works with VLC. + + https://gfycat.com/api + + https://gfycat.com/UntidyAcidicIberianemeraldlizard --> + https://giant.gfycat.com/UntidyAcidicIberianemeraldlizard.webm + """ + pattern = re.compile(r'https?://(www\.)?gfycat\.com/[^.]+$') + + @staticmethod + def get_mimetype(url): + parts = url.split('/') + api_url = '/'.join(parts[:-1] + ['cajax', 'get'] + parts[-1:]) + resp = requests.get(api_url) + image_url = resp.json()['gfyItem']['webmUrl'] + return image_url, 'video/webm' + + +class YoutubeMIMEParser(BaseMIMEParser): + """ + Youtube videos can be streamed with vlc or downloaded with youtube-dl. + Assign a custom mime-type so they can be referenced in mailcap. + """ + pattern = re.compile( + r'(?:https?://)?(m\.)?(?:youtu\.be/|(?:www\.)?youtube\.com/watch' + r'(?:\.php)?\'?.*v=)([a-zA-Z0-9\-_]+)') + + @staticmethod + def get_mimetype(url): + return url, 'video/x-youtube' + + +class GifvMIMEParser(BaseMIMEParser): + """ + Special case for .gifv, which is a custom video format for imgur serves + as html with a special