Skip to content

Commit

Permalink
tubi: added new server
Browse files Browse the repository at this point in the history
Added new ANIME server. At this moment searching is broken and will only
return a subset of results.
  • Loading branch information
TAAPArthur committed May 20, 2024
1 parent 8f4c852 commit d94e53d
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 2 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ assimilate all unread chapters and then read them
* [Crunchyroll](https://crunchyroll.com)
* [Funimation](https://funimation.com)
* [HiDive](https://hidive.com/)
* [Tubi](https://tubitv.com/category/anime)\*^

### Light novels
* [FreeWebNovel](https://freewebnovel.com) (unofficial)
Expand All @@ -104,10 +105,14 @@ assimilate all unread chapters and then read them
* Local Server -- media already downloaded on the machine; see the import subcommand
* Remove Server -- media hosted on some simple webserver (like darkhttpd)

^ - Login isn't fully supported (Note we don't support logging into unofficial servers due to lack of utility)

\* - Search isn't fully operation and only a subset of possible shows will be returned

Optional dependency breakdown
* PIL: required to download manga for Viz and JNovelClub
* beautifulsoup4: required to download images for JNovelClub (only for light novel parts)
* beautifulsoup4: required to enable DB multiverse, FreeWebNovel, Funimation, Nyaa, RemoteServer and Webtoons
* beautifulsoup4: required to enable DB multiverse, FreeWebNovel, Funimation, Nyaa, RemoteServer, Tubi and Webtoons
* beautifulsoup4 : required to search for Crunchyroll (manga)
* cloudscraper: required to enable searching/updating on MangaSee and HumbleBundle
* cloudscraper: potentially required to access all features of Crunchyroll (manga and anime)
Expand Down
2 changes: 1 addition & 1 deletion amt/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def search_for_media(self, term, limit=None, media_type=None, **kwargs):
Searches for a media containing term
Different servers will handle search differently. Some are very literal while others do prefix matching and some would match any word
"""
return find_media_with_similar_name_in_list(get_alt_names(term), self.get_media_list())
return find_media_with_similar_name_in_list(get_alt_names(term), self.get_media_list(search_term=term))

@property
def fuzzy_search(self):
Expand Down
103 changes: 103 additions & 0 deletions amt/servers/tubi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import json
import re

from bs4 import BeautifulSoup

from ..server import Server
from ..util.media_type import MediaType


class Tubi(Server):
id = "tubi"
media_type = MediaType.ANIME
slow_download = True

domain = "tubitv.com"
base_url = f"https://{domain}"

show_url = base_url + "/category/anime/"
search_url = base_url + "/oz/search/{}?isKidsMode=false&useLinearHeader=true"

stream_url_regex = re.compile(f"{domain}/(?:movies|tv-shows)/([^/]*)")
add_series_url_regex = re.compile(f"{domain}/(?:movies|series)/([^/]*)/([^/]*)")

def get_episode_info(self, media_data=None, url=None):
text = self.session_get_cache(url or (self.base_url + media_data["alt_id"]), ttl=90)
text = text.split("window.__data=", 1)[-1].split("</script>")[0].strip()
text = text.replace("undefined", "0")
text = text.replace("new Date(", "").replace("\")", "\"")
text = text[:-1]
return json.loads(text)

def _get_media_list_from_url(self, relative_url, limit=None):
url = self.base_url + relative_url
match = self.add_series_url_regex.search(url)
if match:
data = self.get_episode_info(url=url)
media_id = match.group(1)
series_info = next(filter(lambda x: x["id"] == media_id, data["video"]["byId"].values()))
if series_info["type"] == "s":
for season_info in series_info.get("seasons", [{}]):
yield self.create_media_data(id=media_id, name=series_info["title"], alt_id=relative_url, season_id=season_info["number"], lang=series_info["lang"])
else:
yield self.create_media_data(id=media_id, name=series_info["title"], alt_id=relative_url, lang=series_info["lang"])

def get_media_list(self, limit=None, search_term=None, **kwargs):
r = self.session_get_cache(self.show_url)
soup = self.soupify(BeautifulSoup, r)
term_parts = set(self.non_word_char_regex.split(search_term.lower())) if search_term else None
for link in soup.findAll("a", {"class": "web-content-tile__title"})[:limit]:
if not search_term or self.score_results(term_parts=term_parts, media_name=link.getText()):
yield from self._get_media_list_from_url(link["href"], limit=limit)

"""
def search_for_media(self, term, limit=2, **kwargs):
referer = f"https://tubitv.com/search/{term}"
r = self.session_get(referer)
print(r.cookies)
self.session_set_cookie("latest_viewed_path", f"search/{term}")
self.session_set_cookie("deviceId", "a8650587-341f-4bbc-85e9-dd627cf36356")
data_list = self.session_get_cache_json(self.search_url.format(term), headers={"Referer": referer})
for data in data_list:
label = "movies" if data["type"] == "v" else "series"
yield from self._get_media_list_from_url(f"/{label}/{data['id']}")
"""

def update_media_data(self, media_data, **kwargs):
data = self.get_episode_info(media_data)
series_info = next(filter(lambda x: x["id"] == media_data["id"], data["video"]["byId"].values()))
episode_ids = []
if "seasons" in series_info and not isinstance(series_info["seasons"], int):
season_info = next(filter(lambda x: x["number"] == media_data["season_id"], series_info["seasons"])) if "seasons" in series_info else None
episode_ids = list(map(lambda x: x["id"], season_info["episodes"]))

for episode_metadata in filter(lambda x: x["type"] == "v" and (not episode_ids or x["id"] in episode_ids), data["video"]["byId"].values()):
self.update_chapter_data(media_data, id=episode_metadata["id"], number=episode_metadata.get("episode_number"), title=episode_metadata["title"], lang=episode_metadata["lang"], premium=episode_metadata["needs_login"])

def get_stream_urls(self, media_data, chapter_data):
data = self.get_episode_info(media_data)
return [[x["manifest"]["url"] for x in data["video"]["byId"][chapter_data["id"]]["video_resources"]]]

def get_subtitle_info(self, media_data, chapter_data):
data = self.get_episode_info(media_data)
episode_info = next(filter(lambda x: x["id"] == chapter_data["id"], data["video"]["byId"].values()))
for subtitles in episode_info.get("subtitles", []):
yield subtitles["lang"], subtitles["url"], None, False

def get_all_media_data_from_url(self, url):
match = self.add_series_url_regex.search(url)
relative_url = url.split(self.base_url, 2)[1]
if match:
return list(self._get_media_list_from_url(relative_url))
alt_id = url.split(self.domain)[1].split("?")[0]
data = self.get_episode_info(url=url)

chapter_id = self.get_chapter_id_for_url(url)
series_info = next(filter(lambda x: x["type"] == "s", data["video"]["byId"].values()))
for season_info in series_info["seasons"]:
if chapter_id in map(lambda x: x["id"], season_info["episodes"]):
return [self.create_media_data(id=series_info["id"], name=series_info["title"], alt_id=alt_id, season_id=season_info.get("number"), lang=series_info["lang"])]

def get_chapter_id_for_url(self, url):
return self.stream_url_regex.search(url).group(1)
4 changes: 4 additions & 0 deletions amt/tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2336,6 +2336,9 @@ class ServerStreamTest(RealBaseUnitTestClass):
("https://mangasee123.com/read-online/Berserk-chapter-1-page-1.html", "Berserk", None, "100010"),
("https://mangasee123.com/read-online/Bobobo-Bo-Bo-Bobo-chapter-214-page-1.html", "Bobobo-Bo-Bo-Bobo", None, "102140"),
("https://mangasee123.com/read-online/Onepunch-Man-chapter-147-index-2-page-1.html", "Onepunch-Man", None, "201470"),
("https://tubitv.com/movies/667951/gintama-the-very-final-subbed?start=true", "667951", None, "667951"),
("https://tubitv.com/tv-shows/318565/s04-e01-run-the-curry-of-life?start=true", "1622", "4", "318565"),
("https://tubitv.com/tv-shows/624483/s01-e01-sakura-and-the-strange-magical-book?start=true", "300007490", None, "624483"),
("https://viz.com/shonenjump/one-piece-chapter-1/chapter/5090?action=read", "one-piece", None, "5090"),
("https://webtoons.com/en/drama/lookism/ep-283-hostel-epilogue/viewer?title_no=1049&episode_no=283", "1049", None, "283"),
]
Expand All @@ -2353,6 +2356,7 @@ class ServerStreamTest(RealBaseUnitTestClass):
("https://nyaa.si/view/1047104", "1047104"),
("https://nyaa.si/view/135283", "135283"),
("https://nyaa.si/view/269191", "269191"),
("https://tubitv.com/series/300007490/cardcaptor-sakura?start=true", "300007490"),
("https://viz.com/shonenjump/chapters/my-hero-academia-vigilantes", "my-hero-academia-vigilantes"),
("https://webtoons.com/en/drama/lookism/list?title_no=1049", 1049),
]
Expand Down

0 comments on commit d94e53d

Please sign in to comment.