Merge branch 'master' into themes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.18.0'
|
||||
__version__ = '1.20.0'
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
|
||||
__praw_hash__ = 'ad0dbcf49d5937ffd39e13e45ebcb404b00c582a'
|
||||
__praw_hash__ = 'f0373b788356e212be184590741383cc4747a682'
|
||||
__praw_bundled__ = True
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
###############################################################################
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user