Merge branch 'master' into themes

This commit is contained in:
Michael Lazar
2017-12-07 20:55:44 -05:00
35 changed files with 3996 additions and 298 deletions

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
__version__ = '1.18.0'
__version__ = '1.20.0'

View File

@@ -55,8 +55,10 @@ class OpenGraphMIMEParser(BaseMIMEParser):
def get_mimetype(url):
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')
for og_type in ['og:video:secure_url', 'og:video', 'og:image']:
tag = soup.find('meta', attrs={'property': og_type})
for og_type in ['video', 'image']:
tag = soup.find('meta',
attrs={'property':'og:' + og_type + ':secure_url'}) or \
soup.find('meta', attrs={'property': 'og:' + og_type})
if tag:
return BaseMIMEParser.get_mimetype(tag.get('content'))
return url, None
@@ -206,11 +208,14 @@ class ImgurApiMIMEParser(BaseMIMEParser):
_logger.warning('Imgur API failure, resp %s', r.json())
return cls.fallback(url, domain)
if 'images' in data:
if 'images' in data and len(data['images']) > 1:
# TODO: handle imgur albums with mixed content, i.e. jpeg and gifv
link = ' '.join([d['link'] for d in data['images'] if not d['animated']])
mime = 'image/x-imgur-album'
else:
data = data['images'][0] if 'images' in data else data
# this handles single image galleries
link = data['mp4'] if data['animated'] else data['link']
mime = 'video/mp4' if data['animated'] else data['type']
@@ -371,11 +376,16 @@ class LiveleakMIMEParser(BaseMIMEParser):
if source:
urls.append((source.get('src'), source.get('type')))
# TODO: Handle pages with multiple videos
# TODO: Handle pages with youtube embeds
if urls:
return urls[0]
else:
return url, None
iframe = soup.find_all(lambda t: t.name == 'iframe' and
'youtube.com' in t['src'])
if iframe:
return YoutubeMIMEParser.get_mimetype(iframe[0]['src'].strip('/'))
else:
return url, None
class ClippitUserMIMEParser(BaseMIMEParser):
"""
@@ -391,6 +401,85 @@ class ClippitUserMIMEParser(BaseMIMEParser):
return tag.get(quality[0]), 'video/mp4'
class GifsMIMEParser(OpenGraphMIMEParser):
"""
Gifs.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?gifs\.com/gif/.+$')
class GiphyMIMEParser(OpenGraphMIMEParser):
"""
Giphy.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?giphy\.com/gifs/.+$')
class ImgtcMIMEParser(OpenGraphMIMEParser):
"""
imgtc.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?imgtc\.com/w/.+$')
class ImgflipMIMEParser(OpenGraphMIMEParser):
"""
imgflip.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?imgflip\.com/i/.+$')
class LivememeMIMEParser(OpenGraphMIMEParser):
"""
livememe.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?livememe\.com/[^.]+$')
class MakeamemeMIMEParser(OpenGraphMIMEParser):
"""
makeameme.com uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?makeameme\.org/meme/.+$')
class FlickrMIMEParser(OpenGraphMIMEParser):
"""
Flickr uses the Open Graph protocol
"""
pattern = re.compile(r'https?://(www\.)?flickr\.com/photos/[^/]+/[^/]+/?$')
# TODO: handle albums/photosets (https://www.flickr.com/services/api)
class WorldStarHipHopMIMEParser(BaseMIMEParser):
"""
<video>
<source src="https://hw-mobile.worldstarhiphop.com/..mp4" type="video/mp4">
<source src="" type="video/mp4">
</video>
Sometimes only one video source is available
"""
pattern = re.compile(r'https?://((www|m)\.)?worldstarhiphop\.com/videos/video.php\?v=\w+$')
@staticmethod
def get_mimetype(url):
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')
source = soup.find_all(lambda t: t.name == 'source' and
t['src'] and t['type'] == 'video/mp4')
if source:
return source[0]['src'], 'video/mp4'
else:
iframe = soup.find_all(lambda t: t.name == 'iframe' and
'youtube.com' in t['src'])
if iframe:
return YoutubeMIMEParser.get_mimetype(iframe[0]['src'])
else:
return url, None
# Parsers should be listed in the order they will be checked
parsers = [
ClippitUserMIMEParser,
@@ -405,5 +494,13 @@ parsers = [
YoutubeMIMEParser,
LiveleakMIMEParser,
TwitchMIMEParser,
FlickrMIMEParser,
GifsMIMEParser,
GiphyMIMEParser,
ImgtcMIMEParser,
ImgflipMIMEParser,
LivememeMIMEParser,
MakeamemeMIMEParser,
WorldStarHipHopMIMEParser,
GifvMIMEParser,
BaseMIMEParser]

View File

@@ -33,6 +33,15 @@ def patch_webbrowser():
https://bugs.python.org/issue31348
"""
# Add the suckless.org surf browser, which isn't in the python
# standard library
webbrowser.register('surf', None, webbrowser.BackgroundBrowser('surf'))
# Fix the opera browser, see https://github.com/michael-lazar/rtv/issues/476.
# By default, opera will open a new tab in the current window, which is
# what we want to do anyway.
webbrowser.register('opera', None, webbrowser.BackgroundBrowser('opera'))
if sys.platform != 'darwin' or 'BROWSER' not in os.environ:
return
@@ -83,6 +92,7 @@ def curses_session():
# return from C start_color() is ignorable.
try:
curses.start_color()
curses.use_default_colors()
except:
_logger.warning('Curses failed to initialize color support')
@@ -92,9 +102,6 @@ def curses_session():
except:
_logger.warning('Curses failed to initialize the cursor mode')
# Assign the terminal's default (background) color to code -1
curses.use_default_colors()
yield stdscr
finally:

View File

@@ -10,7 +10,7 @@ from __future__ import absolute_import
import sys
__praw_hash__ = 'ad0dbcf49d5937ffd39e13e45ebcb404b00c582a'
__praw_hash__ = 'f0373b788356e212be184590741383cc4747a682'
__praw_bundled__ = True

View File

@@ -2310,13 +2310,17 @@ class MultiredditMixin(AuthenticatedReddit):
"""
url = self.config['multireddit_about'].format(user=self.user.name,
multi=name)
self.http.headers['x-modhash'] = self.modhash
# The modhash isn't necessary for OAuth requests
if not self._use_oauth:
self.http.headers['x-modhash'] = self.modhash
try:
self.request(url, data={}, method='DELETE', *args, **kwargs)
finally:
del self.http.headers['x-modhash']
if not self._use_oauth:
del self.http.headers['x-modhash']
@decorators.restrict_access(scope='subscribe')
def edit_multireddit(self, *args, **kwargs):
"""Edit a multireddit, or create one if it doesn't already exist.

View File

@@ -115,6 +115,13 @@ class RedditContentObject(object):
"""Set the `name` attribute to `value."""
if value and name == 'subreddit' and isinstance(value, six.string_types):
value = Subreddit(self.reddit_session, value, fetch=False)
elif name == 'permalink' and isinstance(self, Comment):
# The Reddit API now returns the permalink field for comments. This
# will unfortunately break PRAW because permalink is a @property on the
# Comment object. I need to investigate if the property can be removed,
# for now this is a quick hack to get things working again.
# https://github.com/michael-lazar/rtv/issues/462
return
elif value and name in REDDITOR_KEYS:
if isinstance(value, bool):
pass
@@ -1653,6 +1660,13 @@ class Subreddit(Messageable, Refreshable):
class Multireddit(Refreshable):
"""A class for users' Multireddits."""
# 2017-11-13
# Several of the @restrict_access decorators have been removed here,
# because they were duplicated in the corresponding reddit_session
# methods and raised assertion errors. The is the same category of
# bug as this issue:
# https://github.com/praw-dev/praw/issues/477
# Generic listing selectors
get_controversial = _get_sorter('controversial')
get_hot = _get_sorter('')
@@ -1687,6 +1701,15 @@ class Multireddit(Refreshable):
def __init__(self, reddit_session, author=None, name=None,
json_dict=None, fetch=False, **kwargs):
"""Construct an instance of the Multireddit object."""
# When get_my_multireddits is called, we extract the author
# and multireddit name from the path. A trailing forward
# slash was recently added to the path string in the API
# response, the needs to be removed to fix the code.
# path = "/user/redditor/m/multi/"
if json_dict and json_dict['path']:
json_dict['path'] = json_dict['path'].rstrip('/')
author = six.text_type(author) if author \
else json_dict['path'].split('/')[-3]
if not name:
@@ -1735,16 +1758,19 @@ class Multireddit(Refreshable):
url = self.reddit_session.config['multireddit_add'].format(
user=self._author, multi=self.name, subreddit=subreddit)
method = 'DELETE' if _delete else 'PUT'
self.reddit_session.http.headers['x-modhash'] = \
self.reddit_session.modhash
# The modhash isn't necessary for OAuth requests
if not self.reddit_session._use_oauth:
self.reddit_session.http.headers['x-modhash'] = \
self.reddit_session.modhash
data = {'model': dumps({'name': subreddit})}
try:
self.reddit_session.request(url, data=data, method=method,
*args, **kwargs)
finally:
del self.reddit_session.http.headers['x-modhash']
# The modhash isn't necessary for OAuth requests
if not self.reddit_session._use_oauth:
del self.reddit_session.http.headers['x-modhash']
@restrict_access(scope='subscribe')
def copy(self, to_name):
"""Copy this multireddit.
@@ -1756,7 +1782,6 @@ class Multireddit(Refreshable):
return self.reddit_session.copy_multireddit(self._author, self.name,
to_name)
@restrict_access(scope='subscribe')
def delete(self):
"""Delete this multireddit.
@@ -1767,7 +1792,6 @@ class Multireddit(Refreshable):
"""
return self.reddit_session.delete_multireddit(self.name)
@restrict_access(scope='subscribe')
def edit(self, *args, **kwargs):
"""Edit this multireddit.
@@ -1779,12 +1803,10 @@ class Multireddit(Refreshable):
return self.reddit_session.edit_multireddit(name=self.name, *args,
**kwargs)
@restrict_access(scope='subscribe')
def remove_subreddit(self, subreddit, *args, **kwargs):
"""Remove a subreddit from the user's multireddit."""
return self.add_subreddit(subreddit, True, *args, **kwargs)
@restrict_access(scope='subscribe')
def rename(self, new_name, *args, **kwargs):
"""Rename this multireddit.

View File

@@ -391,7 +391,9 @@ class Page(object):
else:
title = sub_name
if os.getenv('DISPLAY'):
# Setting the terminal title will break emacs or systems without
# X window.
if os.getenv('DISPLAY') and not os.getenv('INSIDE_EMACS'):
title += ' - rtv {0}'.format(__version__)
title = self.term.clean(title)
if six.PY3:

View File

@@ -155,13 +155,15 @@ class SubmissionPage(Page):
Open the selected item with the system's pager
"""
n_rows, n_cols = self.term.stdscr.getmaxyx()
data = self.get_selected_item()
if data['type'] == 'Submission':
text = '\n\n'.join((data['permalink'], data['text']))
self.term.open_pager(text)
self.term.open_pager(text, wrap=n_cols)
elif data['type'] == 'Comment':
text = '\n\n'.join((data['permalink'], data['body']))
self.term.open_pager(text)
self.term.open_pager(text, wrap=n_cols)
else:
self.term.flash()

View File

@@ -26,15 +26,15 @@
# Note that rtv returns a list of urls for imgur albums, so we don't put quotes
# around the `%s`
image/x-imgur-album; feh -g 640x480 %s; test=test -n "$DISPLAY"
image/gif; mpv '%s' --autofit 640x480 --loop=inf; test=test -n "$DISPLAY"
image/*; feh -g 640x480 '%s'; test=test -n "$DISPLAY"
# Youtube videos are assigned a custom mime-type, which can be streamed with
# vlc or youtube-dl.
video/x-youtube; vlc '%s' --width 640 --height 480; test=test -n "$DISPLAY"
video/x-youtube; youtube-dl -q -o - '%s' | mpv - --autofit 640x480; test=test -n "$DISPLAY"
video/x-youtube; mpv --ytdl-format=best '%s' --autofit 640x480; test=test -n "$DISPLAY"
# Mpv is a simple and effective video streamer
video/webm; mpv '%s' --autofit 640x480 --loop=inf; test=test -n "$DISPLAY"
video/*; mpv '%s' --autofit 640x480 --loop=inf; test=test -n "$DISPLAY"
###############################################################################

View File

@@ -20,7 +20,7 @@ from tempfile import NamedTemporaryFile
import six
from kitchen.text.display import textual_width_chop
from . import exceptions, mime_parsers
from . import exceptions, mime_parsers, content
from .theme import Theme, ThemeList
from .objects import LoadScreen
@@ -269,9 +269,13 @@ class Terminal(object):
# Trying to draw outside of the screen bounds
return
text = self.clean(text, n_cols)
params = [] if attr is None else [attr]
window.addstr(row, col, text, *params)
try:
text = self.clean(text, n_cols)
params = [] if attr is None else [attr]
window.addstr(row, col, text, *params)
except curses.error as e:
_logger.warning('add_line raised an exception')
_logger.exception(str(e))
@staticmethod
def add_space(window):
@@ -508,9 +512,21 @@ class Terminal(object):
# can re-use the webbrowser instance that has been patched
# by RTV. It's also safer because it doesn't inject
# python code through the command line.
null = open(os.devnull, 'ab+', 0)
sys.stdout, sys.stderr = null, null
webbrowser.open_new_tab(url)
# Surpress stdout/stderr from the browser, see
# https://stackoverflow.com/questions/2323080. We can't
# depend on replacing sys.stdout & sys.stderr because
# webbrowser uses Popen().
stdout, stderr = os.dup(1), os.dup(2)
null = os.open(os.devnull, os.O_RDWR)
try:
os.dup2(null, 1)
os.dup2(null, 2)
webbrowser.open_new_tab(url)
finally:
null.close()
os.dup2(stdout, 1)
os.dup2(stderr, 2)
p = Process(target=open_url_silent, args=(url,))
p.start()
@@ -535,7 +551,7 @@ class Terminal(object):
with self.suspend():
webbrowser.open_new_tab(url)
def open_pager(self, data):
def open_pager(self, data, wrap=None):
"""
View a long block of text using the system's default pager.
@@ -544,6 +560,11 @@ class Terminal(object):
pager = os.getenv('PAGER') or 'less'
command = shlex.split(pager)
if wrap:
data_lines = content.Content.wrap_text(data, wrap)
data = '\n'.join(data_lines)
try:
with self.suspend():
_logger.debug('Running command: %s', command)