Skip to content
This repository has been archived by the owner on Sep 8, 2020. It is now read-only.

Adding AnimeLab streaming links #12

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions config-example.ini
Expand Up @@ -21,9 +21,13 @@ client =
[service.nyaa]
domain = nyaa.se

[service.animelab]
proxy =

[options]
# Valid options, separated with a space: tv, movie, ova
new_show_types = tv
debug = false

[post]
title = [Spoilers] {show_name} - Episode {episode} discussion
Expand Down
9 changes: 9 additions & 0 deletions season_configs/spring_2016.yaml
Expand Up @@ -52,6 +52,7 @@ info:
streams:
crunchyroll: ''
funimation: http://www.funimation.com/shows/my-hero-academia/home
animelab: http://www.animelab.com/shows/my-hero-academia
---
title: Bungou Stray Dogs
type: tv
Expand All @@ -74,6 +75,7 @@ info:
streams:
crunchyroll: ''
funimation: http://www.funimation.com/shows/concrete-revolutio
animelab: http://www.animelab.com/shows/concrete-revolutio|13
---
title: Endride
type: tv
Expand Down Expand Up @@ -107,6 +109,7 @@ info:
streams:
crunchyroll: http://www.crunchyroll.com/the-asterisk-war|12
funimation: http://www.funimation.com/shows/the-asterisk-war|12
animelab: http://www.animelab.com/shows/the-asterisk-war|12
---
title: 'Gyakuten Saiban: Sono "Shinjitsu", Igi Ari! (Ace Attorney)'
type: tv
Expand Down Expand Up @@ -195,6 +198,7 @@ info:
streams:
crunchyroll: ''
funimation: http://www.funimation.com/shows/kumamiko/home
animelab: http://www.animelab.com/shows/kuma-miko-girl-meets-bear
---
title: Kuromukuro
type: tv
Expand All @@ -217,6 +221,7 @@ info:
streams:
crunchyroll: http://www.crunchyroll.com/rin-ne|25
funimation: ''
animelab: https://www.animelab.com/shows/rinne|25
---
title: Mayoiga
type: tv
Expand Down Expand Up @@ -283,6 +288,7 @@ info:
streams:
crunchyroll: ''
funimation: http://www.funimation.com/shows/three-leaves-three-colors/home
animelab: http://www.animelab.com/shows/three-leaves-three-colors-sansha-sanyo
---
title: 'Seisen Cerberus: Ryuukoku no Fatalités'
type: tv
Expand Down Expand Up @@ -327,6 +333,7 @@ info:
streams:
crunchyroll: http://www.crunchyroll.com/tanaka-kun-is-always-listless
funimation: ''
animelab: http://www.animelab.com/shows/tanaka-kun-is-always-listless
---
title: Terra Formars Revenge
type: tv
Expand Down Expand Up @@ -360,6 +367,7 @@ info:
streams:
crunchyroll: http://www.crunchyroll.com/ushio-and-tora|26
funimation: ''
animelab: http://www.animelab.com/shows/ushio-and-tora|26
---
title: Wagamama High Spec
type: tv
Expand Down Expand Up @@ -427,6 +435,7 @@ info:
streams:
crunchyroll: ''
funimation: http://www.funimation.com/shows/assassination-classroom/home|22
animelab: http://www.animelab.com/shows/assassination-classroom|22
---
title: Super Lovers
type: tv
Expand Down
24 changes: 11 additions & 13 deletions src/config.py
Expand Up @@ -47,19 +47,17 @@ def from_file(file_path):
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)

#TODO: make dynamic
if "service.mal" in parsed:
sec = parsed["service.mal"]
config.services["mal"] = {"username": sec.get("username", None), "password": sec.get("password", None)}

if "service.anidb" in parsed:
sec = parsed["service.anidb"]
config.services["anidb"] = {"client": sec.get("client", None)}

if "service.nyaa" in parsed:
sec = parsed["service.nyaa"]
config.services["nyaa"] = {"domain": sec.get("domain", None)}

# Dynamically load service configuration
service_prefix = "service."
for section_name in [sec for sec in parsed.sections() if sec.startswith(service_prefix)]:
sec = parsed[section_name]

service_key = section_name[len(service_prefix):]
service_config = config.services[service_key] = {}

for key in sec.keys():
service_config[key] = sec.get(key)

if "options" in parsed:
sec = parsed["options"]
Expand Down
2 changes: 1 addition & 1 deletion src/module_edit.py
Expand Up @@ -101,7 +101,7 @@ def _edit_with_file(db, edit_file):
else:
service = db.get_service(key=service_key)
s = db.get_stream(service_tuple=(service, show_key))
db.update_stream(s, show_key=show_key, remote_offset=remote_offset, commit=False)
db.update_stream(s, show=show_id, show_key=show_key, remote_offset=remote_offset, commit=False)
else:
error(" Stream handler not installed")

Expand Down
127 changes: 127 additions & 0 deletions src/services/stream/animelab.py
@@ -0,0 +1,127 @@
import calendar
import locale
import re
from datetime import datetime
from logging import debug, warning, error

from data.models import Episode, UnprocessedStream
from .. import AbstractServiceHandler

class ServiceHandler(AbstractServiceHandler):
_show_link_url = "http://www.animelab.com/shows/{key}"
_show_link_url_id_re = re.compile("animelab.com/shows/([\w-]+)", re.I)

_episode_link_url = "http://www.animelab.com/player/{episode}"

_list_seasonal_shows_url = "http://www.animelab.com/api/simulcasts?limit=100&page={page}"
_list_show_episodes_url = "http://www.animelab.com/api/videoentries/show/{id}?limit=30&page={page}"
_list_aired_episodes_url = "http://www.animelab.com/api/simulcasts/episodes/latest?limit=10&page={page}"

def __init__(self):
super().__init__("animelab", "AnimeLab", False)

# Modify all requests to check for and apply proxy
def request(self, url, proxy=None, **kwargs):
if ":" in self.config.get("proxy", ""):
proxy = tuple(self.config["proxy"].split(":"))

lang = locale.getdefaultlocale()[0]
if proxy is None and "AU" not in lang and "NZ" not in lang:
warning("AnimeLab requires an AU/NZ IP, but no proxy was supplied")

return super().request(url, proxy=proxy, **kwargs)

# Episodes are usually delayed at least several hours, so the utility of this will be limited
def get_latest_episode(self, stream, **kwargs):
page_index = 0
page_url = self._list_aired_episodes_url.format(page=page_index)

page_json = self.request(page_url, json=True, **kwargs)
if page_json is None:
error("Failed to get recently aired shows list")
return None

for episode_json in page_json["list"]:
episode_show_key = episode_json["showSlug"]
if stream.show_key == episode_show_key:
return self._episode_from_json(episode_json)

# TODO Use show-specific API if not found in recently aired list
warning("Skipped checking show-specific episode list")

return None

def _episode_from_json(self, episode_json):
episode_number = int(episode_json["episodeNumber"])
episode_name = episode_json["name"]
episode_slug = episode_json["slug"]
episode_link_url = self._episode_link_url.format(episode=episode_slug)
# TODO Given release date is unreliable so ignore it for now
return Episode(episode_number, episode_name, episode_link_url, datetime.utcnow())

def get_stream_info(self, stream, **kwargs):
# TODO Figure out what info might be missing.
return None # or the updated stream

def get_seasonal_streams(self, year=None, season=None, **kwargs):
found_streams = list()

# Perform API request for show list
page_index = 0
page_url = self._list_seasonal_shows_url.format(page=page_index)

page_json = self.request(page_url, json=True, **kwargs)
if page_json is None:
error("Failed to get seasonal shows list")
return found_streams

# Extract streams from response JSON
for show_json in page_json["list"]:
if not self._is_airing_during_season(show_json, year, season):
continue

stream = self._stream_from_json(show_json)
found_streams += [stream]

# TODO Handle multiple pages of results.
remaining_page_count = page_json["totalPageCount"] - 1
if remaining_page_count > 0:
warning("Skipped {} pages of results".format(remaining_page_count))

return found_streams

def _stream_from_json(self, show_json):
show_key = show_json["slug"]
show_name = show_json["name"] # Could also use "originalName"

debug("Found show {}: \"{}\"".format(show_key, show_name))

return UnprocessedStream(self.key, show_key, None, show_name, 0, 0)

@staticmethod
def _is_airing_during_season(show_json, year, season):
if year is None or season is None:
return True

stream_start_timestamp = show_json["simulcastStartDate"] / 1000
stream_end_timestamp = show_json["simulcastEndDate"] / 1000

# TODO Confirm the appropriate values for the season parameter
midseason_dates = {
'winter': datetime(year, 2, 15),
'spring': datetime(year, 5, 15),
'summer': datetime(year, 8, 15),
'autumn': datetime(year, 11, 15),
'fall': datetime(year, 11, 15)
}

midseason_timestamp = calendar.timegm(midseason_dates[season].utctimetuple())

return stream_start_timestamp < midseason_timestamp < stream_end_timestamp

def get_stream_link(self, stream):
return self._show_link_url.format(key=stream.show_key)

def extract_show_key(self, url):
match = self._show_link_url_id_re.search(url)
return match.group(1) if match else None