Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![Holo, of course.](holo.png)

# Holo
Anime episode discussion post bot for [/r/anime](https://reddit.com/r/anime/). Monitors online stream services for newly published episodes and submits a post for each to Reddit.
Anime episode discussion post bot for [/r/anime](https://reddit.com/r/anime/). Monitors online stream services for newly published episodes and submits a post for each to Reddit. Posting to Lemmy communities is also available.

Currently operates under the account [/u/AutoLovepon](https://www.reddit.com/user/AutoLovepon/). (Previously [/u/Holo_of_Yoitsu](https://www.reddit.com/user/Holo_of_Yoitsu/))

Expand All @@ -14,6 +14,7 @@ Season configurations (show names and associated service URLs for each anime sea
* `beautifulsoup4`
* `praw`
* `praw-script-oauth`
* `pythorhead`
* `unidecode`
* `pyyaml`

Expand Down
8 changes: 8 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ database = database.sqlite
[connection]
useragent =

# Reddit backend
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Reddit backend

Just commenting, but I don't think this line is needed, because of the [reddit] directly below it.

[reddit]
subreddit =
username =
password =
oauth_key =
oauth_secret =

# Lemmy alternative backend
# [lemmy]
# instance =
# community =
# username =
# password =

[service.mal]
username =
password =
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pythorhead>=0.10.2
praw>=6.4.0
beautifulsoup4>=4.8.0
feedparser>=5.2.1
Expand Down
43 changes: 34 additions & 9 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ def __init__(self):
self.ratelimit = 1.0

self.subreddit = None
self.backend = None

self.r_username = None
self.r_password = None
self.r_oauth_key = None
self.r_oauth_secret = None

self.l_instance = None
self.l_username = None
self.l_password = None

self.services = dict()

self.new_show_types = list()
Expand Down Expand Up @@ -63,12 +69,21 @@ def from_file(file_path):

if "reddit" in parsed:
sec = parsed["reddit"]
config.backend = "reddit"
config.subreddit = sec.get("subreddit", None)
config.r_username = sec.get("username", None)
config.r_password = sec.get("password", None)
config.r_oauth_key = sec.get("oauth_key", None)
config.r_oauth_secret = sec.get("oauth_secret", None)

if "lemmy" in parsed:
sec = parsed["lemmy"]
config.backend = "lemmy"
config.subreddit = sec.get("community", None)
config.l_instance = sec.get("instance", None)
config.l_username = sec.get("username", None)
config.l_password = sec.get("password", None)

if "options" in parsed:
sec = parsed["options"]
config.debug = sec.getboolean("debug", False)
Expand Down Expand Up @@ -118,15 +133,25 @@ def is_bad_str(s):
warning("Rate limit can't be negative, defaulting to 1.0")
config.ratelimit = 1.0
if is_bad_str(config.subreddit):
return "subreddit missing"
if is_bad_str(config.r_username):
return "reddit username missing"
if is_bad_str(config.r_password):
return "reddit password missing"
if is_bad_str(config.r_oauth_key):
return "reddit oauth key missing"
if is_bad_str(config.r_oauth_secret):
return "reddit oauth secret missing"
return "subreddit/community missing"
if config.backend == "reddit":
if is_bad_str(config.r_username):
return "reddit username missing"
if is_bad_str(config.r_password):
return "reddit password missing"
if is_bad_str(config.r_oauth_key):
return "reddit oauth key missing"
if is_bad_str(config.r_oauth_secret):
return "reddit oauth secret missing"
elif config.backend == "lemmy":
if is_bad_str(config.l_instance):
return "lemmy instance missing"
if is_bad_str(config.l_username):
return "lemmy username missing"
if is_bad_str(config.l_password):
return "lemmy password missing"
else:
return "no backend configured"
if is_bad_str(config.post_title):
return "post title missing"
if is_bad_str(config.post_body):
Expand Down
5 changes: 4 additions & 1 deletion src/holo.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def main(config, args, extra_args):
parser.add_argument("-m", "--module", dest="module", nargs=1, choices=["setup", "edit", "episode", "update", "find", "create", "batch"], default=["episode"], help="runs the specified module")
parser.add_argument("-c", "--config", dest="config_file", nargs=1, default=["config.ini"], help="use or create the specified database location")
parser.add_argument("-d", "--database", dest="db_name", nargs=1, default=None, help="use or create the specified database location")
parser.add_argument("-s", "--subreddit", dest="subreddit", nargs=1, default=None, help="set the subreddit on which to make posts")
parser.add_argument("-s", "--subreddit", "--community", dest="subreddit", nargs=1, default=None, help="set the subreddit/community on which to make posts")
parser.add_argument("-l", "--lemmy-instance", dest="lemmy_instance", nargs=1, default=None, help="set the instance hosting the community (lemmy backend)")
parser.add_argument("-o", "--output", dest="output", nargs=1, default="db", help="set the output mode (db or yaml) if supported")
parser.add_argument("-L", "--log-dir", dest="log_dir", nargs=1, default=["logs"], help="set the log directory")
parser.add_argument("-v", "--version", action="version", version="{} v{}, {}".format(name, version, description))
Expand All @@ -107,6 +108,8 @@ def main(config, args, extra_args):
c.database = args.db_name[0]
if args.subreddit is not None:
c.subreddit = args.subreddit[0]
if args.lemmy_instance is not None:
c.l_instance = args.lemmy_instance[0]

# Start
use_log = args.no_input
Expand Down
77 changes: 77 additions & 0 deletions src/lemmy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from logging import debug, info, warning, error, exception
from pythorhead import Lemmy

# Initialization

_l = None
_config = None

def init_lemmy(config):
global _config
_config = config

def _connect_lemmy():
if _config is None:
error("Can't connect to lemmy without a config")
return None
lemmy = Lemmy(_config.l_instance)
return lemmy if lemmy.log_in(_config.l_username, _config.l_password) else None

def _ensure_connection():
global _l
if _l is None:
_l = _connect_lemmy()
return _l is not None


def _get_post_id_from_shortlink(url):
_ensure_connection()
return int(url.split('/')[-1])

def _extract_post_response(post_data):
if not post_data or not post_data['post_view'] or not post_data['post_view']['post']:
exception(f"Bad post response: {post_data}")
return post_data['post_view']['post']

def _get_host_instance():
if _config.subreddit.find('@') != -1:
return _config.subreddit.split('@')[-1]
else:
return _config.l_instance

# Thing doing

def submit_text_post(community, title, body):
_ensure_connection()
info(f"Submitting post to {community}")
community_id = _l.discover_community(community)
if not community_id:
exception(f"Community {community} not found")
response = _l.post.create(community_id, title, body=body)
return _extract_post_response(response)

def edit_text_post(url, body):
_ensure_connection()
post_id = _get_post_id_from_shortlink(url)
try:
info(f"Editing post {url}")
response = _l.post.edit(post_id, body=body)
return _extract_post_response(response)
except:
exception("Failed to submit text post")
return None

def get_text_post(url):
_ensure_connection()
post_id = _get_post_id_from_shortlink(url)
try:
response = _l.post.get(post_id)
return _extract_post_response(response)
except:
exception("Failed to retrieve text post")
return None

# Utilities

def get_shortlink_from_id(id):
return f"http://{_get_host_instance()}/post/{id}"
23 changes: 16 additions & 7 deletions src/module_batch_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

import services
from data.models import Stream, Episode
import reddit
import reddit, lemmy

from module_find_episodes import _create_reddit_post, _edit_reddit_post, _format_post_text
from module_find_episodes import _create_post, _edit_post, _format_post_text

def main(config, db, show_name, episode_count):
int_episode_count = int(episode_count)
reddit.init_reddit(config)
if config.backend == "reddit":
reddit.init_reddit(config)
elif config.backend == "lemmy":
lemmy.init_lemmy(config)

show = db.get_show_by_name(show_name)
if not show:
Expand All @@ -19,7 +22,7 @@ def main(config, db, show_name, episode_count):
post_urls = list()
for i in range(1, int_episode_count+1):
int_episode = Episode(i, None, None, None)
post_url = _create_reddit_post(config, db, show, stream, int_episode, submit=not config.debug)
post_url = _create_post(config, db, show, stream, int_episode, submit=not config.debug)
info(" Post URL: {}".format(post_url))
if post_url is not None:
post_url = post_url.replace("http:", "https:")
Expand All @@ -29,18 +32,24 @@ def main(config, db, show_name, episode_count):
post_urls.append(post_url)

for editing_episode in db.get_episodes(show):
_edit_reddit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)
_edit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)

megathread_title, megathread_body = _create_megathread_content(config, db, show, stream, episode_count)

if not config.debug:
megathread_post = reddit.submit_text_post(config.subreddit, megathread_title, megathread_body)
if config.backend == "reddit":
megathread_post = reddit.submit_text_post(config.subreddit, megathread_title, megathread_body)
elif config.backend == "lemmy":
megathread_post = lemmy.submit_text_post(config.subreddit, megathread_title, megathread_body)
else:
megathread_post = None

if megathread_post is not None:
debug("Post successful")
megathread_url = reddit.get_shortlink_from_id(megathread_post.id).replace("http:", "https:")
if config.backend == "reddit":
megathread_url = reddit.get_shortlink_from_id(megathread_post.id).replace("http:", "https:")
elif config.backend == "lemmy":
megathread_url = lemmy.get_shortlink_from_id(megathread_post['id']).replace("http:", "https:")
else:
error("Failed to submit post")
megathread_url = None
Expand Down
13 changes: 8 additions & 5 deletions src/module_create_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@

import services
from data.models import Stream, Episode
import reddit
import reddit, lemmy

from module_find_episodes import _create_reddit_post, _edit_reddit_post
from module_find_episodes import _create_post, _edit_post

def main(config, db, show_name, episode):
int_episode = Episode(int(episode), None, None, None)
reddit.init_reddit(config)
if config.backend == "reddit":
reddit.init_reddit(config)
elif config.backend == "lemmy":
lemmy.init_lemmy(config)

show = db.get_show_by_name(show_name)
if not show:
raise IOError(f"Show {show_name} does not exist!")
stream = Stream.from_show(show)

post_url = _create_reddit_post(config, db, show, stream, int_episode, submit=not config.debug)
post_url = _create_post(config, db, show, stream, int_episode, submit=not config.debug)
info(" Post URL: {}".format(post_url))
if post_url is not None:
post_url = post_url.replace("http:", "https:")
db.add_episode(show, int_episode.number, post_url)
if show.delayed:
db.set_show_delayed(show, False)
for editing_episode in db.get_episodes(show):
_edit_reddit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)
_edit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)
return True
else:
error(" Episode not submitted")
Expand Down
38 changes: 25 additions & 13 deletions src/module_find_episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

import services
from data.models import Stream
import reddit
import reddit, lemmy

def main(config, db, **kwargs):
reddit.init_reddit(config)
if config.backend == "reddit":
reddit.init_reddit(config)
elif config.backend == "lemmy":
lemmy.init_lemmy(config)

has_new_episode = []

Expand Down Expand Up @@ -115,7 +118,7 @@ def _process_new_episode(config, db, show, stream, episode):

# New episode!
if not already_seen and not episode_number_gap:
post_url = _create_reddit_post(config, db, show, stream, int_episode, submit=not config.debug)
post_url = _create_post(config, db, show, stream, int_episode, submit=not config.debug)
info(" Post URL: {}".format(post_url))
if post_url is not None:
post_url = post_url.replace("http:", "https:")
Expand All @@ -128,7 +131,7 @@ def _process_new_episode(config, db, show, stream, episode):
edit_history_length = int(4 * 13 / 2) # cols x rows / 2
editing_episodes.sort(key=lambda x: x.number)
for editing_episode in editing_episodes[-edit_history_length:]:
_edit_reddit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)
_edit_post(config, db, show, stream, editing_episode, editing_episode.link, submit=not config.debug)
else:
error(" Episode not submitted")

Expand All @@ -138,25 +141,34 @@ def _process_new_episode(config, db, show, stream, episode):

return False

def _create_reddit_post(config, db, show, stream, episode, submit=True):
def _create_post(config, db, show, stream, episode, submit=True):
display_episode = stream.to_display_episode(episode)

title, body = _create_post_contents(config, db, show, stream, display_episode)
if submit:
new_post = reddit.submit_text_post(config.subreddit, title, body)
if new_post is not None:
debug("Post successful")
return reddit.get_shortlink_from_id(new_post.id)
else:
error("Failed to submit post")
if config.backend == "reddit":
new_post = reddit.submit_text_post(config.subreddit, title, body)
if new_post is not None:
debug("Post successful")
return reddit.get_shortlink_from_id(new_post.id)
elif config.backend == "lemmy":
new_post = lemmy.submit_text_post(config.subreddit, title, body)
if new_post is not None:
debug("Post successful")
return lemmy.get_shortlink_from_id(new_post['id'])

error("Failed to submit post")
return None

def _edit_reddit_post(config, db, show, stream, episode, url, submit=True):
def _edit_post(config, db, show, stream, episode, url, submit=True):
display_episode = stream.to_display_episode(episode)

_, body = _create_post_contents(config, db, show, stream, display_episode, quiet=True)
if submit:
reddit.edit_text_post(url, body)
if config.backend == "reddit":
reddit.edit_text_post(url, body)
if config.backend == "lemmy":
lemmy.edit_text_post(url, body)
return None

def _create_post_contents(config, db, show, stream, episode, quiet=False):
Expand Down