diff --git a/MANIFEST.in b/MANIFEST.in index 1733105..29b4bae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,4 @@ 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..9c2b6d7 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ Auto-generate the config file by running $ rtv --copy-config -See the `default config `_ for the full list of settings. +See the `default config `_ for the full list of settings. ------ Editor diff --git a/rtv/__main__.py b/rtv/__main__.py index c8605ca..bb002df 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -12,7 +12,7 @@ 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 @@ -38,7 +38,7 @@ def main(): logging.captureWarnings(True) if six.PY3: # These ones get triggered even when capturing warnings is turned on - warnings.simplefilter('ignore', ResourceWarning) + warnings.simplefilter('ignore', ResourceWarning) #pylint:disable=E0602 locale.setlocale(locale.LC_ALL, '') @@ -65,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() @@ -109,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