Skip to content

Commit

Permalink
First "real" version of billboard.
Browse files Browse the repository at this point in the history
  • Loading branch information
setrofim committed Apr 20, 2016
1 parent 671c599 commit efa0e8a
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 63 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## billboard

Periodically pulls images and text from Reddit and displays them full-screen.
Inspired by

http://imgur.com/a/Io3xi


### Note

This requires PyQt4 to be installed on the system. It seems it currently cannot
be deployed with pip, so it's not mentioned in the dependencies.


### TODO

- Currently a buch of stuff is hard-coded that should be configurable (e.g.
subpreddits to pull images/text from)
- Add more sources (currenltly, only Reddit)
- An ability to remotely send images/text to the billboard for immediate display
- Support Python 2
- Switch to PyQt5
- ...


### LICENSE

3-clause BSD


Empty file added billboard/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions billboard/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sys
import argparse

from PyQt4.QtGui import QApplication

from billboard.billboard import Billboard
from billboard.display import BillboardDisplay
from billboard.sources.reddit import RedditSource


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--period', type=int, default=60*15,
help='''
Period for switching billboard display in seconds.
Defaults to 15 minutes.
''')
return parser.parse_args(sys.argv[1:])


def main():
args = parse_args()
app = QApplication(sys.argv)

display = BillboardDisplay()
sources = [RedditSource()]

billboard = Billboard(display, sources, args.period)

billboard.start()
display.show()
app.exec_()


if __name__ == '__main__':
main()

24 changes: 24 additions & 0 deletions billboard/billboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import time
from threading import Thread
from itertools import cycle


class Billboard(Thread):

def __init__(self, display, sources, period):
super(Billboard, self).__init__()
self.display = display
self.sources = sources
self.period = period
self.daemon = True

def run(self):
for source in cycle(self.sources):
image, text = source.next()
if image is None and text is None:
continue
if image is not None:
self.display.update_image(image)
if text is not None:
self.display.display_text(text)
time.sleep(self.period)
59 changes: 59 additions & 0 deletions billboard/display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os

from PyQt4.QtCore import Qt, QObject, SIGNAL
from PyQt4.QtGui import (QMainWindow, QWidget, QPixmap, QLabel,
QGraphicsDropShadowEffect, QColor,
QDesktopWidget)


class BillboardDisplay(QMainWindow):

def __init__(self, parent=None, fontsize=42):
super(BillboardDisplay, self).__init__(parent)
desktop = QDesktopWidget()
self.display = QWidget(self)
size = desktop.availableGeometry(desktop.primaryScreen());
self.display.resize(size.width(), size.height())
self.display.setWindowTitle("Billboard")

self.image_label = QLabel(self.display)
self.image_label.resize(size.width(), size.height())

self.text_label = QLabel(self.display)
self.text_label.resize(size.width(), size.height())
self.text_label.setMargin(100)
self.text_label.setStyleSheet('''
QLabel {{
font-size: {}pt;
color: #eeeeee;
text-align: center;
}}
'''.format(fontsize))
self.text_label.setWordWrap(True)
self.text_label.setAlignment(Qt.AlignCenter)

dse = QGraphicsDropShadowEffect()
dse.setBlurRadius(10)
dse.setXOffset(0)
dse.setYOffset(0)
dse.setColor(QColor(0, 0, 0, 255))
self.text_label.setGraphicsEffect(dse)
QObject.connect(self, SIGNAL("updateimage"),
self.display_image)

def update_image(self, imagepath):
self.emit(SIGNAL("updateimage"), imagepath)

def display(self, imagepath, text):
self.display_text(text)
self.display_image(imagepath)
self.showFullScreen()

def display_image(self, imagepath):
pix = QPixmap(imagepath)
self.image_label.setPixmap(pix.scaled(self.display.size(),
Qt.KeepAspectRatioByExpanding))

def display_text(self, text):
self.text_label.setText('"{}"'.format(text))

Empty file added billboard/sources/__init__.py
Empty file.
File renamed without changes.
84 changes: 21 additions & 63 deletions proof_of_concept.py → billboard/sources/reddit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
import os
import sys
import shutil
Expand All @@ -7,18 +6,17 @@

import praw
import requests
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication, QWidget, QPixmap, QLabel
from PyQt4.QtGui import QGraphicsDropShadowEffect, QColor

from billboard.utils import DroppingSet

class ImageGetter():

class ImageGetter:

def __init__(self, reddit, subreddit='earthporn', aspect_ratio=1.6):
self.reddit = reddit
self.subreddit = subreddit
self.aspect_ratio = aspect_ratio
self._seen = set()
self._seen = DroppingSet(50)

def get_image(self, path):
try:
Expand Down Expand Up @@ -46,7 +44,7 @@ def _get_image_url(self, subs):
return source['url']


class TextGetter():
class TextGetter:

def __init__(self, reddit, subreddit='showerthoughts', badlist='bad-words.txt'):
self.reddit = reddit
Expand All @@ -56,7 +54,7 @@ def __init__(self, reddit, subreddit='showerthoughts', badlist='bad-words.txt'):
self.bad_words = fh.read().splitlines()
else:
self.bad_words = []
self._seen = set()
self._seen = DroppingSet(50)

def get_text(self):
try:
Expand All @@ -75,60 +73,20 @@ def get_text(self):
return None


def show_billboard(imagepath, text, fontsize=42):
app = QApplication(sys.argv)
w = QWidget()
w.resize(1600, 1000)
w.setWindowTitle("Billboard")

pix = QPixmap(imagepath)

image_label = QLabel(w)
image_label.resize(1600, 1000)
image_label.setPixmap(pix.scaled(w.size(), Qt.KeepAspectRatioByExpanding))

text_label = QLabel(w)
text_label.resize(1600, 1000)
text_label.setMargin(100)
text_label.setText('"{}"'.format(text))
text_label.setStyleSheet('''
QLabel {{
font-size: {}pt;
color: #eeeeee;
text-align: center;
}}
'''.format(fontsize))
text_label.setWordWrap(True)
text_label.setAlignment(Qt.AlignCenter)

dse = QGraphicsDropShadowEffect()
dse.setBlurRadius(10)
dse.setXOffset(0)
dse.setYOffset(0)
dse.setColor(QColor(0, 0, 0, 255))
text_label.setGraphicsEffect(dse)

w.show()
app.exec_()


def main():
reddit = praw.Reddit(user_agent='billboard')
imagegetter = ImageGetter(reddit)
textgetter = TextGetter(reddit)

image_path = tempfile.mktemp()
if not imagegetter.get_image(image_path):
logging.error("Did not find a suitable image.")
sys.exit(1)
text = textgetter.get_text()
if not text:
logging.error("Did not find a suitable text.")
sys.exit(1)

show_billboard(image_path, text)
os.unlink(image_path)
class RedditSource:

def __init__(self):
self.reddit = praw.Reddit(user_agent='billboard')
self.imagegetter = ImageGetter(self.reddit)
badlist = os.path.join(os.path.dirname(__file__), 'bad-words.txt')
self.textgetter = TextGetter(self.reddit, badlist=badlist)
self.image_path = tempfile.mktemp()
self.logger = logging.getLogger('reddit')

if __name__ == '__main__':
main()
def next(self):
if not self.imagegetter.get_image(self.image_path):
self.logger.error("Did not find a suitable image.")
text = self.textgetter.get_text()
if not text:
self.logger.error("Did not find a suitable text.")
return self.image_path, text
81 changes: 81 additions & 0 deletions billboard/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from collections import MutableSet

# based on OrderedSet recipe:
# https://code.activestate.com/recipes/576694/
class DroppingSet(MutableSet):
"""
A set with a maximum size. It keeps track of the order in which elements
are added. Once max_size is reached, oldest elements are dropped.
"""

@property
def max_size(self):
return self._max_size

@max_size.setter
def max_size(self, value):
self._max_size = value
while len(self) > self._max_size:
self.pop(last=False)

def __init__(self, max_size, iterable=None):
self._max_size = max_size
self.end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.map = {} # key --> [key, prev, next]
if iterable is not None:
self |= iterable

def __len__(self):
return len(self.map)

def __contains__(self, key):
return key in self.map

def add(self, key):
while len(self) >= self.max_size:
self.pop(last=False)
if key not in self.map:
end = self.end
curr = end[1]
curr[2] = end[1] = self.map[key] = [key, curr, end]

def discard(self, key):
if key in self.map:
key, prev, next = self.map.pop(key)
prev[2] = next
next[1] = prev

def __iter__(self):
end = self.end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]

def __reversed__(self):
end = self.end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]

def pop(self, last=True):
if not self:
raise KeyError('set is empty')
key = self.end[1][0] if last else self.end[2][0]
self.discard(key)
return key

def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self))

def __eq__(self, other):
if isinstance(other, OrderedSet):
return len(self) == len(other) and list(self) == list(other)
return set(self) == set(other)

3 changes: 3 additions & 0 deletions scripts/billboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
from billboard.app import main
main()
Loading

0 comments on commit efa0e8a

Please sign in to comment.