style:Formatted codebase to pep8

This commit is contained in:
Benex254
2024-08-05 09:46:56 +03:00
parent 5bdebcae2c
commit fb858e8d04
64 changed files with 816 additions and 755 deletions

5
.gitignore vendored
View File

@@ -2,7 +2,9 @@
*.mp4
*.mp3
*.ass
vids/*
vids
data/
crashdump.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -10,7 +12,6 @@ __pycache__/
anixstream.ini
# C extensions
*.so
data/
# Distribution / packaging
.Python
build/

View File

@@ -0,0 +1,14 @@
# Changes
# # only load trailers when needed
- change how media card loader class works
- create function to get a trailer url and cache it for 360 secs
- refactor the codebase to reflect proposed changes
# # # change how media card loader class works
- remove unnecesarry code
- make the media card function to still work so it doesn't cause a break in existing functionality- add a get trailer function which takes a callback
# # Use one media popup for all the media cards

View File

@@ -1,7 +1,7 @@
from .home_screen import HomeScreenController
from .search_screen import SearchScreenController
from .my_list_screen import MyListScreenController
from .anime_screen import AnimeScreenController
from .crashlog_screen import CrashLogScreenController
from .downloads_screen import DownloadsScreenController
from .help_screen import HelpScreenController
from .crashlog_screen import CrashLogScreenController
from .home_screen import HomeScreenController
from .my_list_screen import MyListScreenController
from .search_screen import SearchScreenController

View File

@@ -1,16 +1,16 @@
from kivy.cache import Cache
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.cache import Cache
from anixstream.Model import AnimeScreenModel
from anixstream.View import AnimeScreenView
from ..Model import AnimeScreenModel
from ..View import AnimeScreenView
Cache.register("data.anime", limit=20, timeout=600)
class AnimeScreenController:
"""The controller for the anime screen
"""
"""The controller for the anime screen"""
def __init__(self, model: AnimeScreenModel):
self.model = model
self.view = AnimeScreenView(controller=self, model=self.model)
@@ -19,7 +19,7 @@ class AnimeScreenController:
return self.view
def update_anime_view(self, id: int, caller_screen_name: str):
"""method called to update the anime screen when a new
"""method called to update the anime screen when a new
Args:
id (int): the anilst id of the anime
@@ -32,7 +32,6 @@ class AnimeScreenController:
data = self.model.get_anime_data(id)
if data[0]:
self.model.anime_id = id
Clock.schedule_once(
lambda _: self.view.update_layout(

View File

@@ -1,13 +1,11 @@
from anixstream.View import CrashLogScreenView
from anixstream.Model import CrashLogScreenModel
from ..View import CrashLogScreenView
from ..Model import CrashLogScreenModel
class CrashLogScreenController:
"""The crash log screen controller
"""
def __init__(self, model:CrashLogScreenModel):
"""The crash log screen controller"""
def __init__(self, model: CrashLogScreenModel):
self.model = model
self.view = CrashLogScreenView(controller=self, model=self.model)
# self.update_anime_view()

View File

@@ -1,15 +1,13 @@
from anixstream.View import DownloadsScreenView
from anixstream.Model import DownloadsScreenModel
from ..Model import DownloadsScreenModel
from ..View import DownloadsScreenView
class DownloadsScreenController:
"""The controller for the download screen
"""
def __init__(self, model:DownloadsScreenModel):
"""The controller for the download screen"""
def __init__(self, model: DownloadsScreenModel):
self.model = model
self.view = DownloadsScreenView(controller=self, model=self.model)
def get_view(self) -> DownloadsScreenView:
return self.view

View File

@@ -1,12 +1,11 @@
from anixstream.View import HelpScreenView
from anixstream.Model import HelpScreenModel
from ..Model import HelpScreenModel
from ..View import HelpScreenView
class HelpScreenController:
"""The help screen controller
"""
def __init__(self, model:HelpScreenModel):
"""The help screen controller"""
def __init__(self, model: HelpScreenModel):
self.model = model
self.view = HelpScreenView(controller=self, model=self.model)

View File

@@ -1,13 +1,12 @@
from inspect import isgenerator
from kivy.clock import Clock
from kivy.logger import Logger
from anixstream.View import HomeScreenView
from anixstream.Model import HomeScreenModel
from anixstream.View.components import MediaCardsContainer
from anixstream.Utility import show_notification
from ..Model import HomeScreenModel
from ..Utility import show_notification
from ..View import HomeScreenView
from ..View.components import MediaCardsContainer
# TODO:Move the update home screen to homescreen.py
@@ -18,9 +17,10 @@ class HomeScreenController:
The controller implements the strategy pattern. The controller connects to
the view to control its actions.
"""
populate_errors = []
def __init__(self, model:HomeScreenModel):
def __init__(self, model: HomeScreenModel):
self.model = model # Model.main_screen.MainScreenModel
self.view = HomeScreenView(controller=self, model=self.model)
# if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
@@ -68,7 +68,7 @@ class HomeScreenController:
Logger.error("Home Screen:Failed to load trending anime")
self.populate_errors.append("trending Anime")
def highest_scored_anime(self):
def highest_scored_anime(self):
most_scored_cards_container = MediaCardsContainer()
most_scored_cards_container.list_name = "Most Scored"
most_scored_cards_generator = self.model.get_most_scored_anime()
@@ -84,7 +84,9 @@ class HomeScreenController:
def recently_updated_anime(self):
most_recently_updated_cards_container = MediaCardsContainer()
most_recently_updated_cards_container.list_name = "Most Recently Updated"
most_recently_updated_cards_generator = self.model.get_most_recently_updated_anime()
most_recently_updated_cards_generator = (
self.model.get_most_recently_updated_anime()
)
if isgenerator(most_recently_updated_cards_generator):
for card in most_recently_updated_cards_generator:
card.screen = self.view
@@ -94,7 +96,7 @@ class HomeScreenController:
Logger.error("Home Screen:Failed to load recently updated anime")
self.populate_errors.append("Most recently updated Anime")
def upcoming_anime(self):
def upcoming_anime(self):
upcoming_cards_container = MediaCardsContainer()
upcoming_cards_container.list_name = "Upcoming Anime"
upcoming_cards_generator = self.model.get_upcoming_anime()
@@ -109,13 +111,15 @@ class HomeScreenController:
def populate_home_screen(self):
self.populate_errors = []
Clock.schedule_once(lambda _:self.trending_anime(),1)
Clock.schedule_once(lambda _:self.highest_scored_anime(),2)
Clock.schedule_once(lambda _:self.popular_anime(),3)
Clock.schedule_once(lambda _: self.favourite_anime(),4)
Clock.schedule_once(lambda _:self.recently_updated_anime(),5)
Clock.schedule_once(lambda _:self.upcoming_anime(),6)
Clock.schedule_once(lambda _: self.trending_anime(), 1)
Clock.schedule_once(lambda _: self.highest_scored_anime(), 2)
Clock.schedule_once(lambda _: self.popular_anime(), 3)
Clock.schedule_once(lambda _: self.favourite_anime(), 4)
Clock.schedule_once(lambda _: self.recently_updated_anime(), 5)
Clock.schedule_once(lambda _: self.upcoming_anime(), 6)
if self.populate_errors:
show_notification(f"Failed to fetch all home screen data",f"Theres probably a problem with your internet connection or anilist servers are down.\nFailed include:{', '.join(self.populate_errors)}")
show_notification(
"Failed to fetch all home screen data",
f"Theres probably a problem with your internet connection or anilist servers are down.\nFailed include:{', '.join(self.populate_errors)}",
)

View File

@@ -1,12 +1,14 @@
from inspect import isgenerator
from kivy.logger import Logger
# from kivy.clock import Clock
from kivy.utils import difference
from anixstream.View import MyListScreenView
from anixstream.Model import MyListScreenModel
from anixstream.Utility import user_data_helper
from ..Model import MyListScreenModel
from ..Utility import user_data_helper
from ..View import MyListScreenView
class MyListScreenController:
"""
@@ -16,24 +18,26 @@ class MyListScreenController:
the view to control its actions.
"""
def __init__(self, model:MyListScreenModel):
self.model = model
def __init__(self, model: MyListScreenModel):
self.model = model
self.view = MyListScreenView(controller=self, model=self.model)
if len(self.requested_update_my_list_screen())>30:
if len(self.requested_update_my_list_screen()) > 30:
self.requested_update_my_list_screen(2)
def get_view(self) -> MyListScreenView:
return self.view
def requested_update_my_list_screen(self,page=None):
def requested_update_my_list_screen(self, page=None):
user_anime_list = user_data_helper.get_user_anime_list()
if animes_to_add:=difference(user_anime_list,self.model.already_in_user_anime_list):
if animes_to_add := difference(
user_anime_list, self.model.already_in_user_anime_list
):
Logger.info("My List Screen:User anime list change;updating screen")
# if thirty:=len(animes_to_add)>30:
# if thirty:=len(animes_to_add)>30:
# self.model.already_in_user_anime_list = user_anime_list[:30]
# else:
anime_cards = self.model.update_my_anime_list_view(animes_to_add,page)
anime_cards = self.model.update_my_anime_list_view(animes_to_add, page)
self.model.already_in_user_anime_list = user_anime_list
if isgenerator(anime_cards):
@@ -42,7 +46,9 @@ class MyListScreenController:
self.view.update_layout(result_card)
return animes_to_add
elif page:
anime_cards = self.model.update_my_anime_list_view(self.model.already_in_user_anime_list,page)
anime_cards = self.model.update_my_anime_list_view(
self.model.already_in_user_anime_list, page
)
# self.model.already_in_user_anime_list = user_anime_list
if isgenerator(anime_cards):
for result_card in anime_cards:

View File

@@ -3,13 +3,12 @@ from inspect import isgenerator
from kivy.clock import Clock
from kivy.logger import Logger
from anixstream.View import SearchScreenView
from anixstream.Model import SearchScreenModel
from ..Model import SearchScreenModel
from ..View import SearchScreenView
class SearchScreenController:
"""The search screen controller
"""
"""The search screen controller"""
def __init__(self, model: SearchScreenModel):
self.model = model
@@ -19,8 +18,7 @@ class SearchScreenController:
return self.view
def update_trending_anime(self):
"""Gets and adds the trending anime to the search screen
"""
"""Gets and adds the trending anime to the search screen"""
trending_cards_generator = self.model.get_trending_anime()
if isgenerator(trending_cards_generator):
self.view.trending_anime_sidebar.clear_widgets()

View File

@@ -1,7 +1,7 @@
from .home_screen import HomeScreenModel
from .search_screen import SearchScreenModel
from .my_list_screen import MyListScreenModel
from .anime_screen import AnimeScreenModel
from .crashlog_screen import CrashLogScreenModel
from .download_screen import DownloadsScreenModel
from .help_screen import HelpScreenModel
from .download_screen import DownloadsScreenModel
from .home_screen import HomeScreenModel
from .my_list_screen import MyListScreenModel
from .search_screen import SearchScreenModel

View File

@@ -1,13 +1,12 @@
from ..libs.anilist import AniList
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
class AnimeScreenModel(BaseScreenModel):
"""the Anime screen model
"""
"""the Anime screen model"""
data = {}
anime_id = 0
def get_anime_data(self,id:int):
def get_anime_data(self, id: int):
return AniList.get_anime(id)

View File

@@ -5,4 +5,3 @@ class DownloadsScreenModel(BaseScreenModel):
"""
Handles the download screen logic
"""

View File

@@ -1,7 +1,6 @@
from .base_model import BaseScreenModel
class HelpScreenModel(BaseScreenModel):
"""
Handles the help screen logic

View File

@@ -1,6 +1,6 @@
from ..libs.anilist import AniList
from ..Utility.media_card_loader import media_card_loader
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
from anixstream.Utility.media_card_loader import MediaCardLoader
class HomeScreenModel(BaseScreenModel):
@@ -12,7 +12,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
@@ -24,7 +24,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
@@ -36,7 +36,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
@@ -48,7 +48,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
@@ -60,7 +60,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
@@ -72,7 +72,7 @@ class HomeScreenModel(BaseScreenModel):
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:

View File

@@ -1,18 +1,24 @@
from anixstream.libs.anilist import AniList
from anixstream.Utility import media_card_loader, show_notification
from .base_model import BaseScreenModel
from anixstream.Utility import MediaCardLoader,show_notification
class MyListScreenModel(BaseScreenModel):
already_in_user_anime_list = []
def update_my_anime_list_view(self,not_yet_in_user_anime_list:list,page=None):
success,self.data = AniList.search(id_in=not_yet_in_user_anime_list,page=page,sort="SCORE_DESC")
if success:
def update_my_anime_list_view(self, not_yet_in_user_anime_list: list, page=None):
success, self.data = AniList.search(
id_in=not_yet_in_user_anime_list, page=page, sort="SCORE_DESC"
)
if success:
return self.media_card_generator()
else:
show_notification(f"Failed to update my list screen view",self.data["Error"])
show_notification(
"Failed to update my list screen view", self.data["Error"]
)
return None
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)

View File

@@ -1,29 +1,33 @@
from ..libs.anilist import AniList
from ..Utility import media_card_loader, show_notification
from .base_model import BaseScreenModel
from anixstream.libs.anilist import AniList
from anixstream.Utility import MediaCardLoader, show_notification
class SearchScreenModel(BaseScreenModel):
data = {}
def get_trending_anime(self):
success,data = AniList.get_trending()
success, data = AniList.get_trending()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
return _data_generator()
else:
return data
def search_for_anime(self,anime_title,**kwargs):
success,self.data = AniList.search(query=anime_title,**kwargs)
if success:
def search_for_anime(self, anime_title, **kwargs):
success, self.data = AniList.search(query=anime_title, **kwargs)
if success:
return self.media_card_generator()
else:
show_notification(f"Failed to search for {anime_title}",self.data.get("Error"))
show_notification(
f"Failed to search for {anime_title}", self.data.get("Error")
)
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
yield media_card_loader.media_card(anime_item)
self.pagination_info = self.data["data"]["Page"]["pageInfo"]

View File

@@ -1,4 +1,4 @@
from .media_card_loader import MediaCardLoader
from .show_notification import show_notification
from .data import themes_available
from .media_card_loader import media_card_loader
from .show_notification import show_notification
from .utils import write_crash

View File

@@ -1,6 +1,7 @@
from typing import TypedDict
import plyer
import os
from typing import TypedDict
import plyer
from .yaml_parser import YamlParser
@@ -10,9 +11,9 @@ class AnimdlConfig(TypedDict):
default_provider: str
quality_string: str
if local_data_path:=os.getenv("LOCALAPPDATA"):
config_dir = os.path.join(local_data_path,".config")
if local_data_path := os.getenv("LOCALAPPDATA"):
config_dir = os.path.join(local_data_path, ".config")
if not os.path.exists(config_dir):
os.mkdir(config_dir)
animdl_config_folder_location = os.path.join(config_dir, "animdl")

View File

@@ -2,5 +2,152 @@
Just contains some useful data used across the codebase
"""
themes_available = ['Aliceblue', 'Antiquewhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'Blanchedalmond', 'Blue', 'Blueviolet', 'Brown', 'Burlywood', 'Cadetblue', 'Chartreuse', 'Chocolate', 'Coral', 'Cornflowerblue', 'Cornsilk', 'Crimson', 'Cyan', 'Darkblue', 'Darkcyan', 'Darkgoldenrod', 'Darkgray', 'Darkgrey', 'Darkgreen', 'Darkkhaki', 'Darkmagenta', 'Darkolivegreen', 'Darkorange', 'Darkorchid', 'Darkred', 'Darksalmon', 'Darkseagreen', 'Darkslateblue', 'Darkslategray', 'Darkslategrey', 'Darkturquoise', 'Darkviolet', 'Deeppink', 'Deepskyblue', 'Dimgray', 'Dimgrey', 'Dodgerblue', 'Firebrick', 'Floralwhite', 'Forestgreen', 'Fuchsia', 'Gainsboro', 'Ghostwhite', 'Gold', 'Goldenrod', 'Gray', 'Grey', 'Green', 'Greenyellow', 'Honeydew', 'Hotpink', 'Indianred', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'Lavenderblush', 'Lawngreen', 'Lemonchiffon', 'Lightblue', 'Lightcoral', 'Lightcyan', 'Lightgoldenrodyellow', 'Lightgreen', 'Lightgray', 'Lightgrey', 'Lightpink', 'Lightsalmon', 'Lightseagreen', 'Lightskyblue', 'Lightslategray', 'Lightslategrey', 'Lightsteelblue', 'Lightyellow', 'Lime', 'Limegreen', 'Linen', 'Magenta', 'Maroon', 'Mediumaquamarine', 'Mediumblue', 'Mediumorchid', 'Mediumpurple', 'Mediumseagreen', 'Mediumslateblue', 'Mediumspringgreen', 'Mediumturquoise', 'Mediumvioletred', 'Midnightblue', 'Mintcream', 'Mistyrose', 'Moccasin', 'Navajowhite', 'Navy', 'Oldlace', 'Olive', 'Olivedrab', 'Orange', 'Orangered', 'Orchid', 'Palegoldenrod', 'Palegreen', 'Paleturquoise', 'Palevioletred', 'Papayawhip', 'Peachpuff', 'Peru', 'Pink', 'Plum', 'Powderblue', 'Purple', 'Red', 'Rosybrown', 'Royalblue', 'Saddlebrown', 'Salmon', 'Sandybrown', 'Seagreen', 'Seashell', 'Sienna', 'Silver', 'Skyblue', 'Slateblue', 'Slategray', 'Slategrey', 'Snow', 'Springgreen', 'Steelblue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'Whitesmoke', 'Yellow',
'Yellowgreen']
themes_available = [
"Aliceblue",
"Antiquewhite",
"Aqua",
"Aquamarine",
"Azure",
"Beige",
"Bisque",
"Black",
"Blanchedalmond",
"Blue",
"Blueviolet",
"Brown",
"Burlywood",
"Cadetblue",
"Chartreuse",
"Chocolate",
"Coral",
"Cornflowerblue",
"Cornsilk",
"Crimson",
"Cyan",
"Darkblue",
"Darkcyan",
"Darkgoldenrod",
"Darkgray",
"Darkgrey",
"Darkgreen",
"Darkkhaki",
"Darkmagenta",
"Darkolivegreen",
"Darkorange",
"Darkorchid",
"Darkred",
"Darksalmon",
"Darkseagreen",
"Darkslateblue",
"Darkslategray",
"Darkslategrey",
"Darkturquoise",
"Darkviolet",
"Deeppink",
"Deepskyblue",
"Dimgray",
"Dimgrey",
"Dodgerblue",
"Firebrick",
"Floralwhite",
"Forestgreen",
"Fuchsia",
"Gainsboro",
"Ghostwhite",
"Gold",
"Goldenrod",
"Gray",
"Grey",
"Green",
"Greenyellow",
"Honeydew",
"Hotpink",
"Indianred",
"Indigo",
"Ivory",
"Khaki",
"Lavender",
"Lavenderblush",
"Lawngreen",
"Lemonchiffon",
"Lightblue",
"Lightcoral",
"Lightcyan",
"Lightgoldenrodyellow",
"Lightgreen",
"Lightgray",
"Lightgrey",
"Lightpink",
"Lightsalmon",
"Lightseagreen",
"Lightskyblue",
"Lightslategray",
"Lightslategrey",
"Lightsteelblue",
"Lightyellow",
"Lime",
"Limegreen",
"Linen",
"Magenta",
"Maroon",
"Mediumaquamarine",
"Mediumblue",
"Mediumorchid",
"Mediumpurple",
"Mediumseagreen",
"Mediumslateblue",
"Mediumspringgreen",
"Mediumturquoise",
"Mediumvioletred",
"Midnightblue",
"Mintcream",
"Mistyrose",
"Moccasin",
"Navajowhite",
"Navy",
"Oldlace",
"Olive",
"Olivedrab",
"Orange",
"Orangered",
"Orchid",
"Palegoldenrod",
"Palegreen",
"Paleturquoise",
"Palevioletred",
"Papayawhip",
"Peachpuff",
"Peru",
"Pink",
"Plum",
"Powderblue",
"Purple",
"Red",
"Rosybrown",
"Royalblue",
"Saddlebrown",
"Salmon",
"Sandybrown",
"Seagreen",
"Seashell",
"Sienna",
"Silver",
"Skyblue",
"Slateblue",
"Slategray",
"Slategrey",
"Snow",
"Springgreen",
"Steelblue",
"Tan",
"Teal",
"Thistle",
"Tomato",
"Turquoise",
"Violet",
"Wheat",
"White",
"Whitesmoke",
"Yellow",
"Yellowgreen",
]

View File

@@ -1,152 +1,21 @@
from collections import deque
from time import sleep
import threading
from kivy.cache import Cache
from kivy.logger import Logger
from pytube import YouTube
from kivy.clock import Clock
from kivy.cache import Cache
from kivy.loader import _ThreadPool
from kivy.logger import Logger
from ..libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
from ..Utility import anilist_data_helper, user_data_helper
from ..View.components import MediaCard
from anixstream.View.components import MediaCard
from anixstream.Utility import anilist_data_helper, user_data_helper
from anixstream.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
Cache.register("trailer_urls.anime", timeout=360)
# Register anime cache in memory
Cache.register("yt_stream_links.anime")
user_anime_list = user_data_helper.get_user_anime_list()
yt_stream_links = user_data_helper.get_anime_trailer_cache()
for link in yt_stream_links:
Cache.append("yt_stream_links.anime", link[0], tuple(link[1]))
# TODO: Make this process more efficient
# for youtube video links gotten from from pytube which is blocking
class MediaCardDataLoader(object):
"""this class loads an anime media card and gets the trailer url from pytube"""
def __init__(self):
self._resume_cond = threading.Condition()
self._num_workers = 5
self._max_upload_per_frame = 5
self._paused = False
self._q_load = deque()
self._q_done = deque()
self._client = []
self._running = False
self._start_wanted = False
self._trigger_update = Clock.create_trigger(self._update)
def start(self):
"""Start the loader thread/process."""
self._running = True
def run(self, *largs):
"""Main loop for the loader."""
pass
def stop(self):
"""Stop the loader thread/process."""
self._running = False
def pause(self):
"""Pause the loader, can be useful during interactions.
.. versionadded:: 1.6.0
"""
self._paused = True
def resume(self):
"""Resume the loader, after a :meth:`pause`.
.. versionadded:: 1.6.0
"""
self._paused = False
self._resume_cond.acquire()
self._resume_cond.notify_all()
self._resume_cond.release()
def _wait_for_resume(self):
while self._running and self._paused:
self._resume_cond.acquire()
self._resume_cond.wait(0.25)
self._resume_cond.release()
def cached_fetch_data(self, yt_watch_url):
data: tuple = Cache.get("yt_stream_links.anime", yt_watch_url) # type: ignore # trailer_url is the yt_watch_link
if not data[0]:
yt = YouTube(yt_watch_url)
preview_image = yt.thumbnail_url
try:
video_stream_url = yt.streams.filter(
progressive=True, file_extension="mp4"
)[-1].url
data = preview_image, video_stream_url
yt_stream_links.append((yt_watch_url, data))
user_data_helper.update_anime_trailer_cache(yt_stream_links)
except:
data = preview_image, None
return data
def _load(self, kwargs):
while len(self._q_done) >= (self._max_upload_per_frame * self._num_workers):
sleep(0.1) # type: ignore
self._wait_for_resume()
yt_watch_link = kwargs["yt_watch_link"]
try:
data = self.cached_fetch_data(yt_watch_link)
except Exception as e:
data = None
Logger.error("Pytube:{e}")
self._q_done.appendleft((yt_watch_link, data))
self._trigger_update()
def _update(self, *largs):
if self._start_wanted:
if not self._running:
self.start()
self._start_wanted = False
# in pause mode, don't unqueue anything.
if self._paused:
self._trigger_update()
return
for _ in range(self._max_upload_per_frame):
try:
yt_watch_link, data = self._q_done.pop()
except IndexError:
return
# update client
for c_yt_watch_link, client in self._client[:]:
if yt_watch_link != c_yt_watch_link:
continue
# got one client to update
if data:
trailer_url = data[1]
if trailer_url:
client.set_trailer_url(trailer_url)
Logger.info(f"Pytube:Found trailer url for {client.title}")
Cache.append("yt_stream_links.anime", yt_watch_link, data)
self._client.remove((c_yt_watch_link, client))
self._trigger_update()
def media_card(
self,
anime_item: AnilistBaseMediaDataSchema,
load_callback=None,
post_callback=None,
**kwargs,
):
media_card = MediaCard()
media_card.anime_id = anime_id = anime_item["id"]
@@ -207,7 +76,7 @@ class MediaCardDataLoader(object):
anime_item["genres"]
)
if anime_id in user_anime_list:
if anime_id in user_data_helper.get_user_anime_list():
media_card.is_in_my_list = True
if anime_item["averageScore"]:
@@ -216,61 +85,26 @@ class MediaCardDataLoader(object):
for i in range(stars):
media_card.stars[i] = 1
# Setting up trailer info to be gotten if available
if anime_item["trailer"]:
yt_watch_link = "https://youtube.com/watch?v=" + anime_item["trailer"]["id"]
data = Cache.get("yt_stream_links.anime", yt_watch_link) # type: ignore # trailer_url is the yt_watch_link
if data:
if data[1] not in (None, False):
media_card.set_preview_image(data[0])
media_card.set_trailer_url(data[1])
return media_card
else:
# if data is None, this is really the first time
self._client.append((yt_watch_link, media_card))
self._q_load.appendleft(
{
"yt_watch_link": yt_watch_link,
"load_callback": load_callback,
"post_callback": post_callback,
"current_anime": anime_item["id"],
"kwargs": kwargs,
}
)
if not kwargs.get("nocache", False):
Cache.append("yt_stream_links.anime", yt_watch_link, (False, False))
self._start_wanted = True
self._trigger_update()
if trailer := anime_item.get("trailer"):
trailer_url = "https://youtube.com/watch/v=" + trailer["id"]
media_card._trailer_url = trailer_url
return media_card
class LoaderThreadPool(MediaCardDataLoader):
def __init__(self):
super(LoaderThreadPool, self).__init__()
self.pool: _ThreadPool | None = None
def start(self):
super(LoaderThreadPool, self).start()
self.pool = _ThreadPool(self._num_workers)
Clock.schedule_interval(self.run, 0)
def stop(self):
super(LoaderThreadPool, self).stop()
Clock.unschedule(self.run)
self.pool.stop() # type: ignore
def run(self, *largs):
while self._running:
try:
parameters = self._q_load.pop()
except:
return
self.pool.add_task(self._load, parameters) # type: ignore
def get_trailer_from_pytube(self, trailer_url, anime):
if trailer := Cache.get("trailer_urls.anime", trailer_url):
return trailer
try:
yt = YouTube(trailer_url)
trailer = yt.streams.filter(
progressive=True,
file_extension="mp4",
)[-1].url
Logger.info(f"Pytube Success:For {anime}")
Cache.append("trailer_urls.anime", trailer_url, trailer)
return trailer
except Exception as e:
Logger.error(f"Pytube Failure:For {anime} reason: {e}")
return ""
MediaCardLoader = LoaderThreadPool()
Logger.info(
"MediaCardLoader: using a thread pool of {} workers".format(
MediaCardLoader._num_workers
)
)
media_card_loader = MediaCardDataLoader()

View File

@@ -1,4 +1,3 @@
# Of course, "very flexible Python" allows you to do without an abstract
# superclass at all or use the clever exception `NotImplementedError`. In my
# opinion, this can negatively affect the architecture of the application.

View File

@@ -1,5 +1,5 @@
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText, MDSnackbarSupportingText
from kivy.clock import Clock
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarSupportingText, MDSnackbarText
def show_notification(title, details):

View File

@@ -2,16 +2,24 @@
Contains Helper functions to read and write the user data files
"""
from kivy.storage.jsonstore import JsonStore
import os
from datetime import date, datetime
from kivy.logger import Logger
from kivy.resources import resource_find
from kivy.logger import Logger
from kivy.storage.jsonstore import JsonStore
app_dir = os.path.dirname(__file__)
data_folder = os.path.join(app_dir, "data")
today = date.today()
now = datetime.now()
user_data = JsonStore(resource_find("user_data.json"))
yt_cache = JsonStore(resource_find("yt_cache.json"))
if user_data_path := os.path.exists(os.path.join(data_folder, "user_data.json")):
user_data = JsonStore(user_data_path)
else:
os.makedirs(data_folder, exist_ok=True)
user_data_path = os.path.join(data_folder, "user_data.json")
user_data = JsonStore(user_data_path)
# Get the user data
@@ -63,20 +71,3 @@ else:
t = 4
yt_anime_trailer_cache_name = f"{today}{t}"
def get_anime_trailer_cache() -> list:
try:
return yt_cache["yt_stream_links"][f"{yt_anime_trailer_cache_name}"]
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_anime_trailer_cache(yt_stream_links: list):
try:
yt_cache.put(
"yt_stream_links", **{f"{yt_anime_trailer_cache_name}": yt_stream_links}
)
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")

View File

@@ -1,6 +1,7 @@
from datetime import datetime
import shutil
import os
import shutil
from datetime import datetime
# TODO: make it use color_text instead of fixed vals
# from .kivy_markup_helper import color_text
@@ -12,7 +13,7 @@ def write_crash(e: Exception):
try:
with open("crashdump.txt", "a") as file:
file.write(error)
except:
except Exception:
with open("crashdump.txt", "w") as file:
file.write(error)
return index
@@ -23,9 +24,10 @@ def read_crash_file():
if not os.path.exists(crash_file_path):
return None
else:
with open(crash_file_path,"r") as file:
with open(crash_file_path, "r") as file:
return file.read()
def move_file(source_path, dest_path):
try:
path = shutil.move(source_path, dest_path)

View File

@@ -1,7 +1,7 @@
import yaml
import os
import yaml
class YamlParser:
"""makes managing yaml files easier"""
@@ -15,7 +15,7 @@ class YamlParser:
try:
with open(self.file_path, "r") as yaml_file:
self.data = yaml.safe_load(yaml_file)
except:
except Exception:
self.data = default
with open(file_path, "w") as yaml_file:
yaml.dump(default, yaml_file)

View File

@@ -1,17 +1,15 @@
from kivy.properties import DictProperty, ObjectProperty, StringProperty
from kivy.properties import ObjectProperty, DictProperty, StringProperty
from anixstream.Utility import anilist_data_helper
from anixstream.libs.anilist import AnilistBaseMediaDataSchema
from anixstream.View.base_screen import BaseScreenView
from ...libs.anilist import AnilistBaseMediaDataSchema
from ...Utility import anilist_data_helper
from ...View.base_screen import BaseScreenView
from .components import (
AnimeHeader,
AnimeSideBar,
AnimeDescription,
AnimeReviews,
AnimeCharacters,
AnimdlStreamDialog,
AnimeCharacters,
AnimeDescription,
AnimeHeader,
AnimeReviews,
AnimeSideBar,
DownloadAnimeDialog,
RankingsBar,
)
@@ -19,6 +17,7 @@ from .components import (
class AnimeScreenView(BaseScreenView):
"""The anime screen view"""
caller_screen_name = StringProperty()
header: AnimeHeader = ObjectProperty()
side_bar: AnimeSideBar = ObjectProperty()
@@ -117,12 +116,12 @@ class AnimeScreenView(BaseScreenView):
self.anime_reviews.reviews = data["reviews"]["nodes"]
def stream_anime_with_custom_cmds_dialog(self,mpv=False):
def stream_anime_with_custom_cmds_dialog(self, mpv=False):
"""
Called when user wants to stream with custom commands
"""
AnimdlStreamDialog(self.data,mpv).open()
AnimdlStreamDialog(self.data, mpv).open()
def open_download_anime_dialog(self):
"""

View File

@@ -1,10 +1,9 @@
from .side_bar import AnimeSideBar
from .header import AnimeHeader
from .rankings_bar import RankingsBar
from .animdl_stream_dialog import AnimdlStreamDialog
from .characters import AnimeCharacters
from .controls import Controls
from .description import AnimeDescription
from .characters import AnimeCharacters
from .review import AnimeReviews
from .animdl_stream_dialog import AnimdlStreamDialog
from .download_anime_dialog import DownloadAnimeDialog
from .header import AnimeHeader
from .rankings_bar import RankingsBar
from .review import AnimeReviews
from .side_bar import AnimeSideBar

View File

@@ -1,12 +1,11 @@
from kivy.clock import Clock
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
CommonElevationBehavior,
StencilBehavior,
)
class AnimdlStreamDialog(
@@ -65,5 +64,6 @@ class AnimdlStreamDialog(
app.watch_on_animdl(custom_options=cmds)
self.dismiss()
def stream_anime(self, app):
Clock.schedule_once(lambda _: self._stream_anime(app))

View File

@@ -1,8 +1,9 @@
from kivy.clock import Clock
from kivy.properties import ObjectProperty, ListProperty
from kivy.properties import ListProperty, ObjectProperty
from kivymd.uix.boxlayout import MDBoxLayout
from ....Utility.anilist_data_helper import format_anilist_date_object
class AnimeCharacter(MDBoxLayout):
"""an Anime character data"""
@@ -27,10 +28,6 @@ class AnimeCharacters(MDBoxLayout):
characters = ListProperty()
def update_characters_card(self, instance, characters):
format_date = lambda date_: (
f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else ""
)
self.container.clear_widgets()
for character_ in characters: # character (character,actor)
character = character_[0]
@@ -40,7 +37,7 @@ class AnimeCharacters(MDBoxLayout):
anime_character.character = {
"name": character["name"]["full"],
"gender": character["gender"],
"dateOfBirth": format_date(character["dateOfBirth"]),
"dateOfBirth": format_anilist_date_object(character["dateOfBirth"]),
"image": character["image"]["medium"],
"age": character["age"],
"description": character["description"],

View File

@@ -1,5 +1,4 @@
from kivy.properties import ObjectProperty
from kivymd.uix.gridlayout import MDGridLayout

View File

@@ -1,5 +1,4 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout

View File

@@ -1,11 +1,10 @@
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
CommonElevationBehavior,
StencilBehavior,
)
# from main import AniXStreamApp

View File

@@ -1,5 +1,4 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout

View File

@@ -1,5 +1,4 @@
from kivy.properties import DictProperty
from kivymd.uix.boxlayout import MDBoxLayout

View File

@@ -1,6 +1,5 @@
from kivy.properties import ObjectProperty, ListProperty
from kivy.clock import Clock
from kivy.properties import ListProperty, ObjectProperty
from kivymd.uix.boxlayout import MDBoxLayout

View File

@@ -1,7 +1,6 @@
from kivy.properties import ObjectProperty, StringProperty, DictProperty, ListProperty
from kivy.utils import get_hex_from_color
from kivy.factory import Factory
from kivy.properties import DictProperty, ListProperty, ObjectProperty, StringProperty
from kivy.utils import get_hex_from_color
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.label import MDLabel

View File

@@ -1,8 +1,8 @@
from kivy.properties import StringProperty
from anixstream.View.base_screen import BaseScreenView
from anixstream.Utility.utils import read_crash_file
from anixstream.Utility.kivy_markup_helper import color_text, bolden
from ...Utility.kivy_markup_helper import bolden, color_text
from ...Utility.utils import read_crash_file
from ...View.base_screen import BaseScreenView
class CrashLogScreenView(BaseScreenView):

View File

@@ -1,12 +1,13 @@
from kivy.properties import StringProperty
from kivymd.uix.boxlayout import MDBoxLayout
# TODO: add a progress bar to show the individual progress of each task
class TaskCard(MDBoxLayout):
anime_task_name = StringProperty()
episodes_to_download = StringProperty()
def __init__(self, anime_title:str, episodes:str, *args, **kwargs):
def __init__(self, anime_title: str, episodes: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.anime_task_name = f"{anime_title}"
self.episodes_to_download = f"Episodes: {episodes}"
self.episodes_to_download = f"Episodes: {episodes}"

View File

@@ -1,9 +1,8 @@
from kivy.clock import Clock
from kivy.properties import ObjectProperty
from kivy.logger import Logger
from kivy.utils import format_bytes_to_human
from anixstream.View.base_screen import BaseScreenView
from ...View.base_screen import BaseScreenView
from .components.task_card import TaskCard

View File

@@ -1,8 +1,8 @@
from kivy.properties import ObjectProperty, StringProperty
from anixstream.View.base_screen import BaseScreenView
from anixstream.Utility.kivy_markup_helper import bolden, color_text, underline
from anixstream.Utility.data import themes_available
from ...Utility.data import themes_available
from ...Utility.kivy_markup_helper import bolden, color_text, underline
from ...View.base_screen import BaseScreenView
class HelpScreenView(BaseScreenView):

View File

@@ -1,6 +1,6 @@
from kivy.properties import ObjectProperty
from anixstream.View.base_screen import BaseScreenView
from ...View.base_screen import BaseScreenView
class HomeScreenView(BaseScreenView):

View File

@@ -1,7 +1,7 @@
from kivy.properties import ObjectProperty, StringProperty, DictProperty
from kivy.clock import Clock
from kivy.properties import DictProperty, ObjectProperty, StringProperty
from anixstream.View.base_screen import BaseScreenView
from ...View.base_screen import BaseScreenView
class MyListScreenView(BaseScreenView):

View File

@@ -1,5 +1,4 @@
from kivy.properties import StringProperty, DictProperty
from kivy.properties import DictProperty, StringProperty
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.menu import MDDropdownMenu

View File

@@ -1,5 +1,4 @@
from kivy.properties import ObjectProperty, NumericProperty
from kivy.properties import NumericProperty, ObjectProperty
from kivymd.uix.boxlayout import MDBoxLayout

View File

@@ -1,8 +1,9 @@
from kivy.properties import ObjectProperty, StringProperty
from kivy.clock import Clock
from kivy.properties import ObjectProperty, StringProperty
from anixstream.View.base_screen import BaseScreenView
from .components import TrendingAnimeSideBar, Filters, SearchResultsPagination
from ...View.base_screen import BaseScreenView
from .components import Filters, SearchResultsPagination, TrendingAnimeSideBar
class SearchScreenView(BaseScreenView):

View File

@@ -1,8 +1,8 @@
# screens
from .HomeScreen.home_screen import HomeScreenView
from .SearchScreen.search_screen import SearchScreenView
from .MylistScreen.my_list_screen import MyListScreenView
from .AnimeScreen.anime_screen import AnimeScreenView
from .CrashLogScreen.crashlog_screen import CrashLogScreenView
from .DownloadsScreen.download_screen import DownloadsScreenView
from .HelpScreen.help_screen import HelpScreenView
from .HomeScreen.home_screen import HomeScreenView
from .MylistScreen.my_list_screen import MyListScreenView
from .SearchScreen.search_screen import SearchScreenView

View File

@@ -1,13 +1,12 @@
from kivy.properties import ObjectProperty, StringProperty
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDIconButton
from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem
from kivymd.uix.screen import MDScreen
from kivymd.uix.tooltip import MDTooltip
from anixstream.Utility.observer import Observer
from ..Utility.observer import Observer
class NavRail(MDNavigationRail):

View File

@@ -1,4 +1,5 @@
from kivy.uix.modalview import ModalView
class AnimdlDialogPopup(ModalView):
pass
pass

View File

@@ -1,10 +1,11 @@
from kivy.uix.videoplayer import VideoPlayer
# TODO: make fullscreen exp better
class MediaPopupVideoPlayer(VideoPlayer):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def on_fullscreen(self, instance, value):
super().on_fullscreen(instance, value)
# self.state = "pause"
# self.state = "pause"

View File

@@ -1,14 +1,14 @@
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.properties import ObjectProperty
from kivy.uix.modalview import ModalView
from kivy.utils import QueryDict
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
StencilBehavior,
CommonElevationBehavior,
HoverBehavior,
StencilBehavior,
)
@@ -20,14 +20,47 @@ class MediaPopup(
BackgroundColorBehavior,
ModalView,
):
caller = ObjectProperty()
caller = ObjectProperty(
QueryDict(
{
"anime_id": "",
"title": "",
"is_play": "",
"trailer_url": "",
"episodes": "",
"favourites": "",
"popularity": "",
"media_status": "",
"is_in_my_list": False,
"is_in_my_notify": False,
"genres": "",
"first_aired_on": "",
"description": "",
"tags": "",
"studios": "",
"next_airing_episode": "",
"producers": "",
"stars": [0, 0, 0, 0, 0, 0],
"cover_image_url": "",
"preview_image": "",
"has_trailer_color": [0, 0, 0, 0],
}
)
)
player = ObjectProperty()
def __init__(self, caller=None, *args, **kwarg):
self.caller = caller
def __init__(self, *args, **kwarg):
# self.caller = caller
super(MediaPopup, self).__init__(*args, **kwarg)
self.player.bind(fullscreen=self.handle_clean_fullscreen_transition)
def update_caller(self, caller):
self.caller = caller
def on_caller(self, *args):
Clock.schedule_once(lambda _: self.apply_class_lang_rules(), -1)
def open(self, *_args, **kwargs):
"""Display the modal in the Window.
@@ -61,7 +94,6 @@ class MediaPopup(
super().open(*_args, **kwargs)
def _align_center(self, *_args):
if self.caller:
if self._is_open:
self.center = self.caller.to_window(*self.caller.center)
@@ -83,13 +115,10 @@ class MediaPopup(
if not fullscreen:
if not self._is_open:
instance.state = "stop"
# if vid := instance._video:
# vid.unload()
if vid := instance._video:
vid.unload()
else:
instance.state = "stop"
# if vid := instance._video:
# vid.unload()
if vid := instance._video:
vid.unload()
self.dismiss()
media_card_popup = MediaPopup()

View File

@@ -13,7 +13,7 @@
width: dp(100)
height: dp(150)
MDDivider:
color:root.has_trailer_color
color:root.theme_cls.primaryColor if root._trailer_url else [0.5, 0.5, 0.5, 0.5]
SingleLineLabel:
font_style:"Label"
role:"medium"

View File

@@ -1,17 +1,17 @@
from kivy.clock import Clock
from kivy.properties import (
ObjectProperty,
StringProperty,
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
StringProperty,
)
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.app import MDApp
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from .components.media_popup import media_card_popup, MediaPopup
from .components.media_popup import MediaPopup
class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
@@ -20,6 +20,7 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
title = StringProperty()
is_play = ObjectProperty()
trailer_url = StringProperty()
_trailer_url: str | None = StringProperty()
episodes = StringProperty()
favourites = StringProperty()
popularity = StringProperty()
@@ -42,6 +43,8 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
super().__init__(**kwargs)
self.orientation = "vertical"
self.app: MDApp | None = MDApp.get_running_app()
if trailer_url:
self.trailer_url = trailer_url
self.adaptive_size = True
@@ -63,21 +66,38 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
def on_dismiss(self, popup: MediaPopup):
popup.player.state = "stop"
# if popup.player._video:
# popup.player._video.unload()
if popup.player._video:
popup.player._video.unload()
def set_preview_image(self, image):
self.preview_image = image
def set_trailer_url(self, trailer_url):
self.trailer_url = trailer_url
self.has_trailer_color = self.theme_cls.primaryColor
def open(self, *_):
media_card_popup.caller = self
media_card_popup.title = self.title
media_card_popup.bind(on_dismiss=self.on_dismiss, on_open=self.on_popup_open)
media_card_popup.open(self)
if app := self.app:
popup: MediaPopup = app.media_card_popup
# self.popup.caller = self
popup.update_caller(self)
popup.title = self.title
popup.bind(on_dismiss=self.on_dismiss, on_open=self.on_popup_open)
popup.open(self)
# trailer stuff
from ....Utility.media_card_loader import media_card_loader
if trailer := self._trailer_url:
# from ....Utility import show_notification
# TODO: show an indefinate progress while traile is still not available
# show_notification("Pytube", "Please wait for trailer to load")
if trailer_url := media_card_loader.get_trailer_from_pytube(
trailer, self.title
):
self.trailer_url = trailer_url
else:
self._trailer_url = None
# ---------------respond to user actions and call appropriate model-------------------------
def on_is_in_my_list(self, instance, in_user_anime_list):

View File

@@ -1,23 +1,22 @@
from anixstream.Controller import (
SearchScreenController,
HomeScreenController,
MyListScreenController,
from ..Controller import (
AnimeScreenController,
CrashLogScreenController,
DownloadsScreenController,
HelpScreenController,
CrashLogScreenController,
HomeScreenController,
MyListScreenController,
SearchScreenController,
)
from anixstream.Model import (
HomeScreenModel,
SearchScreenModel,
MyListScreenModel,
from ..Model import (
AnimeScreenModel,
CrashLogScreenModel,
DownloadsScreenModel,
HelpScreenModel,
CrashLogScreenModel,
HomeScreenModel,
MyListScreenModel,
SearchScreenModel,
)
screens = {
"home screen": {
"model": HomeScreenModel,

View File

@@ -1,83 +1,68 @@
import os
import random
os.environ["KIVY_VIDEO"] = "ffpyplayer"
from queue import Queue
from threading import Thread
from subprocess import Popen
import webbrowser
from queue import Queue
from subprocess import Popen
from threading import Thread
import plyer
from kivy.clock import Clock
from kivy.config import Config
from kivy.resources import resource_find,resource_add_path,resource_remove_path
from kivy.loader import Loader
from kivy.logger import Logger
from kivy.resources import resource_add_path, resource_find, resource_remove_path
from kivy.uix.screenmanager import FadeTransition, ScreenManager
from kivy.uix.settings import Settings, SettingsWithSidebar
from kivymd.app import MDApp
from .libs.animdl import AnimdlApi
from .Utility import (
animdl_config_manager,
show_notification,
themes_available,
user_data_helper,
)
from .Utility.utils import write_crash
from .View.components.media_card.components.media_popup import MediaPopup
from .View.screens import screens
os.environ["KIVY_VIDEO"] = "ffpyplayer" # noqa: E402
Config.set("graphics", "width", "1000") # noqa: E402
Config.set("graphics", "minimum_width", "1000") # noqa: E402
Config.set("kivy", "window_icon", resource_find("logo.ico")) # noqa: E402
Config.write() # noqa: E402
# resource_add_path("_internal")
app_dir = os.path.dirname(__file__)
# test
# test_end
# make sure we aint searching dist folder
dist_folder = os.path.join(app_dir,"dist")
dist_folder = os.path.join(app_dir, "dist")
resource_remove_path(dist_folder)
assets_folder = os.path.join(app_dir,"assets")
assets_folder = os.path.join(app_dir, "assets")
resource_add_path(assets_folder)
conigs_folder = os.path.join(app_dir,"configs")
conigs_folder = os.path.join(app_dir, "configs")
resource_add_path(conigs_folder)
data_folder = os.path.join(app_dir,"data")
resource_add_path(data_folder)
Config.set("graphics","width","1000")
Config.set("graphics","minimum_width","1000")
Config.set('kivy', 'window_icon', resource_find("logo.ico"))
Config.write()
# from kivy.core.window import Window
from kivy.loader import Loader
Loader.num_workers = 5
Loader.max_upload_per_frame = 10
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.uix.screenmanager import ScreenManager, FadeTransition
from kivy.uix.settings import SettingsWithSidebar, Settings
from kivymd.icon_definitions import md_icons
from kivymd.app import MDApp
from anixstream.View.screens import screens
from anixstream.libs.animdl import AnimdlApi
from anixstream.Utility import (
themes_available,
show_notification,
user_data_helper,
animdl_config_manager,
)
from anixstream.Utility.utils import write_crash
# Ensure the user data fields exist
if not (user_data_helper.user_data.exists("user_anime_list")):
user_data_helper.update_user_anime_list([])
if not (user_data_helper.yt_cache.exists("yt_stream_links")):
user_data_helper.update_anime_trailer_cache([])
# TODO: Confirm data integrity from user_data and yt_cache
class AniXStreamApp(MDApp):
# some initialize
queue = Queue()
downloads_queue = Queue()
animdl_streaming_subprocess: Popen | None = None
default_anime_image = resource_find(random.choice(["default_1.jpg","default.jpg"]))
default_banner_image = resource_find(random.choice(["banner_1.jpg","banner.jpg"]))
default_anime_image = resource_find(random.choice(["default_1.jpg", "default.jpg"]))
default_banner_image = resource_find(random.choice(["banner_1.jpg", "banner.jpg"]))
# default_video = resource_find("Billyhan_When you cant afford Crunchyroll to watch anime.mp4")
def worker(self, queue: Queue):
@@ -140,8 +125,11 @@ class AniXStreamApp(MDApp):
return self.manager_screens
def on_start(self, *args):
if self.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
Clock.schedule_once(lambda _:self.home_screen.controller.populate_home_screen(),1)
self.media_card_popup = MediaPopup()
if self.config.get("Preferences", "is_startup_anime_enable") == "1": # type: ignore
Clock.schedule_once(
lambda _: self.home_screen.controller.populate_home_screen(), 1
)
def generate_application_screens(self) -> None:
for i, name_screen in enumerate(screens.keys()):
@@ -153,7 +141,6 @@ class AniXStreamApp(MDApp):
self.manager_screens.add_widget(view)
def build_config(self, config):
# General settings setup
if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore
downloads_dir = os.path.join(vid_path, "anixstream")
@@ -278,38 +265,44 @@ class AniXStreamApp(MDApp):
self.add_anime_to_user_downloads_list(anime_id)
# TODO:Add custom download cmds functionality
on_progress = lambda *args: self.download_screen.on_episode_download_progress(
*args
)
def on_progress(*args):
return self.download_screen.on_episode_download_progress(*args)
output_path = self.config.get("Preferences", "downloads_dir") # type: ignore
self.download_screen.on_new_download_task(
default_cmds["title"], default_cmds.get("episodes_range")
)
if episodes_range := default_cmds.get("episodes_range"):
download_task = lambda: AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
episodes_range,
) # ,default_cmds["quality"]
def download_task():
return AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
episodes_range,
) # ,default_cmds["quality"]
self.downloads_queue.put(download_task)
Logger.info(
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
)
else:
download_task = lambda: AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
) # ,default_cmds.get("quality")
def download_task():
return AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
) # ,default_cmds.get("quality")
self.downloads_queue.put(download_task)
Logger.info(
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
@@ -333,6 +326,7 @@ class AniXStreamApp(MDApp):
show_notification(
"Failure", f"Failed to open {title_} in browser on allanime site"
)
return
if webbrowser.open(parsed_link):
Logger.info(
f"AniXStream:Successfully opened {title} in browser allanime site"
@@ -348,8 +342,8 @@ class AniXStreamApp(MDApp):
"Failure", f"Failed to open {title} in browser on allanime site"
)
except Exception as e:
show_notification("Something went wrong",f"{e}")
show_notification("Something went wrong", f"{e}")
def stream_anime_with_custom_input_cmds(self, *cmds):
self.animdl_streaming_subprocess = (
AnimdlApi._run_animdl_command_and_get_subprocess(["stream", *cmds])
@@ -371,8 +365,8 @@ class AniXStreamApp(MDApp):
# TODO: End mpv child process properly
for stream in streams:
try:
if isinstance(stream,str):
show_notification("Failed to stream current episode",f"{stream}")
if isinstance(stream, str):
show_notification("Failed to stream current episode", f"{stream}")
continue
self.animdl_streaming_subprocess = stream
@@ -384,7 +378,7 @@ class AniXStreamApp(MDApp):
del stream
return
except Exception as e:
show_notification("Something went wrong while streaming",f"{e}")
show_notification("Something went wrong while streaming", f"{e}")
def watch_on_animdl(
self,
@@ -407,20 +401,24 @@ class AniXStreamApp(MDApp):
self.stop_streaming = True
if stream_with_mpv_options:
stream_func = lambda: self.stream_anime_with_mpv(
stream_with_mpv_options["title"],
stream_with_mpv_options.get("episodes_range"),
stream_with_mpv_options["quality"],
)
def stream_func():
return self.stream_anime_with_mpv(
stream_with_mpv_options["title"],
stream_with_mpv_options.get("episodes_range"),
stream_with_mpv_options["quality"],
)
self.queue.put(stream_func)
Logger.info(
f"Animdl:Successfully started to stream {stream_with_mpv_options['title']}"
)
else:
stream_func = lambda: self.stream_anime_with_custom_input_cmds(
*custom_options
)
def stream_func():
return self.stream_anime_with_custom_input_cmds(*custom_options)
self.queue.put(stream_func)
show_notification("Streamer", "Started streaming")
@@ -428,8 +426,14 @@ class AniXStreamApp(MDApp):
def run_app():
AniXStreamApp().run()
if __name__ == "__main__":
try:
in_development = True
if in_development:
run_app()
except Exception as e:
write_crash(e)
else:
try:
run_app()
except Exception as e:
write_crash(e)

View File

@@ -1,6 +1,6 @@
"""
This module contains an abstraction for interaction with the anilist api making it easy and efficient
his module contains an abstraction for interaction with the anilist api making it easy and efficient
"""
from .anilist import AniList
from .anilist_data_schema import AnilistBaseMediaDataSchema
from .anilist_data_schema import AnilistBaseMediaDataSchema

View File

@@ -1,6 +1,7 @@
"""
This is the core module availing all the abstractions of the anilist api
"""
import requests
from .queries_graphql import (
@@ -15,17 +16,21 @@ from .queries_graphql import (
anime_relations_query,
airing_schedule_query,
upcoming_anime_query,
anime_query
)
anime_query,
)
from .anilist_data_schema import AnilistDataSchema
# from kivy.network.urlrequest import UrlRequestRequests
class AniList:
"""
This class provides an abstraction for the anilist api
This class provides an abstraction for the anilist api
"""
@classmethod
def get_data(cls,query:str,variables:dict = {})->tuple[bool,AnilistDataSchema]:
def get_data(
cls, query: str, variables: dict = {}
) -> tuple[bool, AnilistDataSchema]:
"""
The core abstraction for getting data from the anilist api
@@ -39,58 +44,69 @@ class AniList:
url = "https://graphql.anilist.co"
# req=UrlRequestRequests(url, cls.got_data,)
try:
# TODO: check if data is as expected
response = requests.post(url,json={"query":query,"variables":variables},timeout=5)
anilist_data:AnilistDataSchema = response.json()
return (True,anilist_data)
# TODO: check if data is as expected
response = requests.post(
url, json={"query": query, "variables": variables}, timeout=5
)
anilist_data: AnilistDataSchema = response.json()
return (True, anilist_data)
except requests.exceptions.Timeout:
return (False,{"Error":"Timeout Exceeded for connection there might be a problem with your internet or anilist is down."}) # type: ignore
return (
False,
{
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
},
) # type: ignore
except requests.exceptions.ConnectionError:
return (False,{"Error":"There might be a problem with your internet or anilist is down."}) # type: ignore
return (
False,
{
"Error": "There might be a problem with your internet or anilist is down."
},
) # type: ignore
except Exception as e:
return (False,{"Error":f"{e}"}) # type: ignore
return (False, {"Error": f"{e}"}) # type: ignore
@classmethod
def search(cls,
query:str|None=None,
sort:list[str]|None=None,
genre_in:list[str]|None=None,
id_in:list[int]|None=None,
genre_not_in:list[str]=["hentai"],
popularity_greater:int|None=None,
popularity_lesser:int|None=None,
averageScore_greater:int|None=None,
averageScore_lesser:int|None=None,
tag_in:list[str]|None=None,
tag_not_in:list[str]|None=None,
status:str|None = None,
status_in:list[str]|None=None,
status_not_in:list[str]|None=None,
endDate_greater:int|None=None,
endDate_lesser:int|None=None,
start_greater:int|None=None,
start_lesser:int|None=None,
page:int|None=None
):
def search(
cls,
query: str | None = None,
sort: str | None = None,
genre_in: list[str] | None = None,
id_in: list[int] | None = None,
genre_not_in: list[str] = ["hentai"],
popularity_greater: int | None = None,
popularity_lesser: int | None = None,
averageScore_greater: int | None = None,
averageScore_lesser: int | None = None,
tag_in: list[str] | None = None,
tag_not_in: list[str] | None = None,
status: str | None = None,
status_in: list[str] | None = None,
status_not_in: list[str] | None = None,
endDate_greater: int | None = None,
endDate_lesser: int | None = None,
start_greater: int | None = None,
start_lesser: int | None = None,
page: int | None = None,
):
"""
A powerful method for searching anime using the anilist api availing most of its options
"""
variables = {}
variables = {}
for key, val in list(locals().items())[1:]:
if val is not None and key not in ["variables"]:
variables[key] = val
search_results = cls.get_data(search_query,variables=variables)
search_results = cls.get_data(search_query, variables=variables)
return search_results
@classmethod
def get_anime(cls,id:int):
def get_anime(cls, id: int):
"""
Gets a single anime by a valid anilist anime id
"""
variables = {
"id":id
}
return cls.get_data(anime_query,variables)
variables = {"id": id}
return cls.get_data(anime_query, variables)
@classmethod
def get_trending(cls):
@@ -131,38 +147,36 @@ class AniList:
"""
most_popular = cls.get_data(most_popular_query)
return most_popular
# FIXME:dont know why its not giving useful data
@classmethod
def get_recommended_anime_for(cls,id:int):
def get_recommended_anime_for(cls, id: int):
recommended_anime = cls.get_data(recommended_query)
return recommended_anime
@classmethod
def get_charcters_of(cls,id:int):
variables = {"id":id}
characters = cls.get_data(anime_characters_query,variables)
def get_charcters_of(cls, id: int):
variables = {"id": id}
characters = cls.get_data(anime_characters_query, variables)
return characters
@classmethod
def get_related_anime_for(cls,id:int):
variables = {"id":id}
related_anime = cls.get_data(anime_relations_query,variables)
return related_anime
def get_related_anime_for(cls, id: int):
variables = {"id": id}
related_anime = cls.get_data(anime_relations_query, variables)
return related_anime
@classmethod
def get_airing_schedule_for(cls,id:int):
variables = {"id":id}
airing_schedule = cls.get_data(airing_schedule_query,variables)
def get_airing_schedule_for(cls, id: int):
variables = {"id": id}
airing_schedule = cls.get_data(airing_schedule_query, variables)
return airing_schedule
@classmethod
def get_upcoming_anime(cls,page:int):
def get_upcoming_anime(cls, page: int):
"""
Gets upcoming anime from anilist
"""
variables = {"page":page}
upcoming_anime = cls.get_data(upcoming_anime_query,variables)
variables = {"page": page}
upcoming_anime = cls.get_data(upcoming_anime_query, variables)
return upcoming_anime

View File

@@ -1,156 +1,163 @@
"""
This module defines the shape of the anilist data that can be received in order to enhance dev experience
"""
from typing import TypedDict
class AnilistMediaTitle(TypedDict):
english:str
romaji:str
english: str
romaji: str
class AnilistImage(TypedDict):
medium:str
extraLarge:str
small:str
large:str
medium: str
extraLarge: str
small: str
large: str
class AnilistMediaTrailer(TypedDict):
id:str
site:str
id: str
site: str
class AnilistStudio(TypedDict):
name:str
favourites:int
isAnimationStudio:bool
name: str
favourites: int
isAnimationStudio: bool
class AnilistStudioNodes(TypedDict):
nodes:list[AnilistStudio]
nodes: list[AnilistStudio]
class AnilistMediaTag(TypedDict):
name:str
rank:int
name: str
rank: int
class AnilistDateObject(TypedDict):
day:int
month:int
year:int
day: int
month: int
year: int
class AnilistMediaNextAiringEpisode(TypedDict):
timeUntilAiring:int
airingAt:int
episode:int
timeUntilAiring: int
airingAt: int
episode: int
class AnilistUser(TypedDict):
name:str
avatar:AnilistImage
name: str
avatar: AnilistImage
class AnilistReview(TypedDict):
summary:str
user:AnilistUser
summary: str
user: AnilistUser
class AnilistReviewNodes(TypedDict):
nodes:list[AnilistReview]
nodes: list[AnilistReview]
class AnilistMediaRanking(TypedDict):
rank:int
context:str
rank: int
context: str
class AnilistExternalLink(TypedDict):
url:str
site:str
icon:str
url: str
site: str
icon: str
class AnilistName(TypedDict):
full:str
full: str
class AnilistCharacter(TypedDict):
name:AnilistName
gender:str|None
dateOfBirth:AnilistDateObject|None
age:int
image:AnilistImage
description:str
name: AnilistName
gender: str | None
dateOfBirth: AnilistDateObject | None
age: int
image: AnilistImage
description: str
# class AnilistCharacterNode(TypedDict):
# node
class AnilistVoiceActor(TypedDict):
name:AnilistName
image:AnilistImage
name: AnilistName
image: AnilistImage
class AnilistCharactersEdge(TypedDict):
node:list[AnilistCharacter]
voiceActors:list[AnilistVoiceActor]
node: list[AnilistCharacter]
voiceActors: list[AnilistVoiceActor]
class AnilistCharactersEdges(TypedDict):
edges:list[AnilistCharactersEdge]
edges: list[AnilistCharactersEdge]
class AnilistBaseMediaDataSchema(TypedDict):
"""
This a convenience class is used to type the received Anilist data to enhance dev experience
"""
id:str
title:AnilistMediaTitle
coverImage:AnilistImage
trailer:AnilistMediaTrailer|None
popularity:int
favourites:int
averageScore:int
genres:list[str]
episodes:int|None
description:str|None
studios:AnilistStudioNodes
tags:list[AnilistMediaTag]
startDate:AnilistDateObject
endDate:AnilistDateObject
status:str
nextAiringEpisode:AnilistMediaNextAiringEpisode
season:str
seasonYear:int
duration:int
synonyms:list[str]
countryOfOrigin:str
source:str
hashtag:str|None
siteUrl:str
reviews:AnilistReviewNodes
bannerImage:str|None
rankings:list[AnilistMediaRanking]
externalLinks:list[AnilistExternalLink]
characters:AnilistCharactersEdges
format:str
id: str
title: AnilistMediaTitle
coverImage: AnilistImage
trailer: AnilistMediaTrailer | None
popularity: int
favourites: int
averageScore: int
genres: list[str]
episodes: int | None
description: str | None
studios: AnilistStudioNodes
tags: list[AnilistMediaTag]
startDate: AnilistDateObject
endDate: AnilistDateObject
status: str
nextAiringEpisode: AnilistMediaNextAiringEpisode
season: str
seasonYear: int
duration: int
synonyms: list[str]
countryOfOrigin: str
source: str
hashtag: str | None
siteUrl: str
reviews: AnilistReviewNodes
bannerImage: str | None
rankings: list[AnilistMediaRanking]
externalLinks: list[AnilistExternalLink]
characters: AnilistCharactersEdges
format: str
class AnilistPageInfo(TypedDict):
total:int
perPage:int
currentPage:int
hasNextPage:bool
total: int
perPage: int
currentPage: int
hasNextPage: bool
class AnilistPage(TypedDict):
media:list[AnilistBaseMediaDataSchema]
pageInfo:AnilistPageInfo
media: list[AnilistBaseMediaDataSchema]
pageInfo: AnilistPageInfo
class AnilistPages(TypedDict):
Page: AnilistPage
class AnilistDataSchema(TypedDict):
data:AnilistPages
Error:str
data: AnilistPages
Error: str

View File

@@ -1,35 +1,32 @@
import os
import time
import re
import shutil
from subprocess import Popen, run, PIPE, CompletedProcess
from subprocess import PIPE, CompletedProcess, Popen, run
from typing import Callable
from .extras import Logger
from .animdl_data_helper import (
anime_title_percentage_match,
filter_broken_streams,
filter_streams_by_quality,
parse_stream_urls_data,
path_parser,
search_output_parser,
anime_title_percentage_match,
parse_stream_urls_data,
)
from .animdl_exceptions import (
AnimdlAnimeUrlNotFoundException,
InvalidAnimdlCommandsException,
MPVNotFoundException,
NoValidAnimeStreamsException,
Python310NotFoundException,
)
from .animdl_types import AnimdlAnimeEpisode, AnimdlAnimeUrlAndTitle, AnimdlData
from .extras import Logger
broken_link_pattern = r"https://tools.fast4speed.rsvp/\w*"
def run_mpv_command(*cmds) -> Popen:
if mpv := shutil.which("mpv"):
Logger.info({"Animdl Api: Started mpv command"})
Logger.debug({"Animdl Api: Started mpv command"})
child_process = Popen(
[mpv, *cmds],
stderr=PIPE,
@@ -58,7 +55,7 @@ class AnimdlApi:
CompletedProcess: the completed animdl process
"""
if py_path := shutil.which("python"):
Logger.info("Animdl Api: Started Animdl command")
Logger.debug("Animdl Api: Started Animdl command")
if capture:
return run(
[py_path, "-m", "animdl", *cmds],
@@ -91,7 +88,7 @@ class AnimdlApi:
parsed_cmds = list(cmds)
if py_path := shutil.which("python"):
Logger.info("Animdl Api: Started Animdl command")
Logger.debug("Animdl Api: Started Animdl command")
base_cmds = [py_path, "-m", "animdl"]
cmds_ = [*base_cmds, *parsed_cmds]
child_process = Popen(cmds_)
@@ -190,15 +187,15 @@ class AnimdlApi:
subs = ";".join(subtitles)
cmds = [*cmds, f"--sub-files={subs}"]
Logger.info(
Logger.debug(
f"Animdl Api Mpv Streamer: Starting to stream on mpv with commands: {cmds}"
)
yield run_mpv_command(*cmds)
Logger.info(
Logger.debug(
f"Animdl Api Mpv Streamer: Finished to stream episode {episode['episode']} on mpv"
)
else:
Logger.info(
Logger.debug(
f"Animdl Api Mpv Streamer: Failed to stream episode {episode['episode']} no valid streams"
)
yield f"Epiosde {episode['episode']} doesnt have any valid stream links"
@@ -299,7 +296,7 @@ class AnimdlApi:
if not os.path.exists(download_location):
os.mkdir(download_location)
Logger.info(f"Animdl Api Downloader: Started downloading: {anime_title}")
Logger.debug(f"Animdl Api Downloader: Started downloading: {anime_title}")
for episode in anime_streams_data.episodes:
episode_number = episode["episode"]
episode_title = f"Episode {episode_number}"
@@ -338,7 +335,7 @@ class AnimdlApi:
# check if its adaptive or progressive and call the appropriate downloader
if stream_url and subtitles and audio_tracks:
Logger.info(
Logger.debug(
f"Animdl api Downloader: Downloading adaptive episode {anime_title}-{episode_title}"
)
cls.download_adaptive(
@@ -351,8 +348,8 @@ class AnimdlApi:
)
elif stream_url and subtitles:
# probably wont occur
Logger.info(
f"Animdl api Downloader: downloading ? episode {anime_title}-{episode_title}"
Logger.debug(
f"Animdl api Downloader: downloading !? episode {anime_title}-{episode_title}"
)
cls.download_video_and_subtitles(
stream_url,
@@ -362,7 +359,7 @@ class AnimdlApi:
episode_info,
)
else:
Logger.info(
Logger.debug(
f"Animdl api Downloader: Downloading progressive episode {anime_title}-{episode_title}"
)
cls.download_progressive(
@@ -375,16 +372,16 @@ class AnimdlApi:
# epiosode download complete
on_episode_download_complete(anime_title, episode_title)
successful_downloads.append(episode_number)
Logger.info(
Logger.debug(
f"Animdl api Downloader: Success in dowloading {anime_title}-{episode_title}"
)
except Exception as e:
Logger.info(
Logger.debug(
f"Animdl api Downloader: Failed in dowloading {anime_title}-{episode_title}; reason {e}"
)
failed_downloads.append(episode_number)
Logger.info(
Logger.debug(
f"Animdl api Downloader: Completed in dowloading {anime_title}-{episodes_range}; Successful:{len(successful_downloads)}, Failed:{len(failed_downloads)}"
)
on_complete(successful_downloads, failed_downloads, anime_title)
@@ -443,9 +440,10 @@ class AnimdlApi:
)
file_name = episode + ".mp4"
download_location = os.path.join(output_path, file_name)
on_progress_ = lambda current_bytes, total_bytes: on_progress(
current_bytes, total_bytes, episode_info
)
def on_progress_(current_bytes, total_bytes):
return on_progress(current_bytes, total_bytes, episode_info)
isfailure = cls.download_with_mpv(video_url, download_location, on_progress_)
if isfailure:
raise Exception
@@ -474,9 +472,9 @@ class AnimdlApi:
Exception: incase anything goes wrong
"""
on_progress_ = lambda current_bytes, total_bytes: on_progress(
current_bytes, total_bytes, episode_info
)
def on_progress_(current_bytes, total_bytes):
return on_progress(current_bytes, total_bytes, episode_info)
episode = (
path_parser(episode_info["anime_title"])
+ " - "
@@ -484,13 +482,11 @@ class AnimdlApi:
)
sub_filename = episode + ".ass"
sub_filepath = os.path.join(output_path, sub_filename)
is_sub_failure = cls.download_with_mpv(sub_url, sub_filepath, on_progress_)
cls.download_with_mpv(sub_url, sub_filepath, on_progress_)
audio_filename = episode + ".mp3"
audio_filepath = os.path.join(output_path, audio_filename)
is_audio_failure = cls.download_with_mpv(
audio_url, audio_filepath, on_progress_
)
cls.download_with_mpv(audio_url, audio_filepath, on_progress_)
video_filename = episode + ".mp4"
video_filepath = os.path.join(output_path, video_filename)
@@ -523,9 +519,9 @@ class AnimdlApi:
Exception: when anything goes wrong
"""
on_progress_ = lambda current_bytes, total_bytes: on_progress(
current_bytes, total_bytes, episode_info
)
def on_progress_(current_bytes, total_bytes):
return on_progress(current_bytes, total_bytes, episode_info)
episode = (
path_parser(episode_info["anime_title"])
+ " - "
@@ -533,7 +529,7 @@ class AnimdlApi:
)
sub_filename = episode + ".ass"
sub_filepath = os.path.join(output_path, sub_filename)
is_sub_failure = cls.download_with_mpv(sub_url, sub_filepath, on_progress_)
cls.download_with_mpv(sub_url, sub_filepath, on_progress_)
video_filename = episode + ".mp4"
video_filepath = os.path.join(output_path, video_filename)

View File

@@ -1,15 +1,14 @@
import re
import json
import re
from fuzzywuzzy import fuzz
from .extras import Logger
from .animdl_types import (
AnimdlAnimeUrlAndTitle,
AnimdlAnimeEpisode,
AnimdlAnimeUrlAndTitle,
AnimdlEpisodeStream,
)
from .extras import Logger
# Currently this links don't work so we filter it out
broken_link_pattern = r"https://tools.fast4speed.rsvp/\w*"
@@ -65,7 +64,7 @@ def anime_title_percentage_match(
"""
percentage_ratio = fuzz.ratio(title, possible_user_requested_anime_title)
Logger.info(
Logger.debug(
f"Animdl Api Fuzzy: Percentage match of {possible_user_requested_anime_title} against {title}: {percentage_ratio}%"
)
return percentage_ratio
@@ -83,9 +82,11 @@ def filter_broken_streams(
list[AnimdlEpisodeStream]: the valid streams
"""
stream_filter = lambda stream: (
True if not re.match(broken_link_pattern, stream["stream_url"]) else False
)
def stream_filter(stream):
return (
True if not re.match(broken_link_pattern, stream["stream_url"]) else False
)
return list(filter(stream_filter, streams))
@@ -104,9 +105,9 @@ def filter_streams_by_quality(
"""
# get the appropriate stream or default to best
get_quality_func = lambda stream_: (
stream_.get("quality") if stream_.get("quality") else 0
)
def get_quality_func(stream_):
return stream_.get("quality") if stream_.get("quality") else 0
match quality:
case "best":
return max(anime_episode_streams, key=get_quality_func)
@@ -118,7 +119,7 @@ def filter_streams_by_quality(
return episode_stream
else:
# if not strict:
Logger.info(f"Animdl Api: Not strict so defaulting to best")
Logger.debug("Animdl Api: Not strict so defaulting to best")
return max(anime_episode_streams, key=get_quality_func)
# else:
# Logger.warning(
@@ -195,6 +196,6 @@ def search_output_parser(raw_data: str) -> list[AnimdlAnimeUrlAndTitle]:
parsed_data.append(AnimdlAnimeUrlAndTitle(anime_title, data[(i + 1)]))
else:
parsed_data.append(AnimdlAnimeUrlAndTitle(anime_title, item[1]))
except:
except Exception:
pass
return parsed_data # anime title,url

View File

@@ -15,4 +15,4 @@ class NoValidAnimeStreamsException(Exception):
class InvalidAnimdlCommandsException(Exception):
pass
pass

View File

@@ -5,19 +5,20 @@ class AnimdlAnimeUrlAndTitle(NamedTuple):
anime_title: str
animdl_anime_url: str
class AnimdlEpisodeStream(TypedDict):
stream_url:str
quality:int
subtitle:list[str] | None
stream_url: str
quality: int
subtitle: list[str] | None
audio_tracks: list[str] | None
title:str|None
title: str | None
class AnimdlAnimeEpisode(TypedDict):
episode:int
streams:list[AnimdlEpisodeStream]
class AnimdlData(NamedTuple):
anime_title:str
episodes:list[AnimdlAnimeEpisode]
episode: int
streams: list[AnimdlEpisodeStream]
class AnimdlData(NamedTuple):
anime_title: str
episodes: list[AnimdlAnimeEpisode]

View File

@@ -1,7 +1,7 @@
import logging
Logger = logging.getLogger(__name__)
Logger.setLevel(logging.DEBUG)
# Logger.setLevel(logging.DEBUG)
# formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
# console_handler = logging.StreamHandler()
# console_handler.setLevel(logging.INFO)

4
pyrightconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"venv": ".venv",
"venvPath": "."
}