doc and style: formatted the whole codebase to pep8 plus added documentation where necessary

This commit is contained in:
Benex254
2024-08-05 09:46:54 +03:00
parent 8bf06cd34b
commit 32b99834c4
54 changed files with 555 additions and 346 deletions

View File

@@ -1,3 +1,4 @@
{
"python.analysis.typeCheckingMode": "basic"
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true
}

View File

@@ -9,6 +9,8 @@ Cache.register("data.anime", limit=20, timeout=600)
class AnimeScreenController:
"""The controller for the anime screen
"""
def __init__(self, model: AnimeScreenModel):
self.model = model
self.view = AnimeScreenView(controller=self, model=self.model)
@@ -17,6 +19,12 @@ 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
Args:
id (int): the anilst id of the anime
caller_screen_name (str): the screen thats calling this method; used internally to switch back to this screen
"""
if self.model.anime_id != id:
if cached_anime_data := Cache.get("data.anime", f"{id}"):
data = cached_anime_data

View File

@@ -1,11 +1,12 @@
from inspect import isgenerator
from View import CrashLogScreenView
from Model import CrashLogScreenModel
from View.components import MediaCardsContainer
from Utility import show_notification
from kivy.clock import Clock
class CrashLogScreenController:
"""The crash log screen controller
"""
def __init__(self, model:CrashLogScreenModel):
self.model = model
self.view = CrashLogScreenView(controller=self, model=self.model)

View File

@@ -1,28 +1,15 @@
from inspect import isgenerator
from View import DownloadsScreenView
from Model import DownloadsScreenModel
from View.components import MediaCardsContainer
from Utility import show_notification
from kivy.clock import Clock
class DownloadsScreenController:
"""The controller for the download screen
"""
def __init__(self, model:DownloadsScreenModel):
self.model = model
self.view = DownloadsScreenView(controller=self, model=self.model)
# self.update_anime_view()
def get_view(self) -> DownloadsScreenView:
return self.view
# def requested_update_my_list_screen(self):
# 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):
# Logger.info("My List Screen:User anime list change;updating screen")
# anime_cards = self.model.update_my_anime_list_view(animes_to_add)
# self.model.already_in_user_anime_list = user_anime_list
# if isgenerator(anime_cards):
# for result_card in anime_cards:
# result_card.screen = self.view
# self.view.update_layout(result_card)

View File

@@ -1,15 +1,14 @@
from inspect import isgenerator
from View import HelpScreenView
from Model import HelpScreenModel
from View.components import MediaCardsContainer
from Utility import show_notification
from kivy.clock import Clock
class HelpScreenController:
"""The help screen controller
"""
def __init__(self, model:HelpScreenModel):
self.model = model
self.view = HelpScreenView(controller=self, model=self.model)
# self.update_anime_view()
def get_view(self) -> HelpScreenView:
return self.view

View File

@@ -13,7 +13,7 @@ from Utility import show_notification
# TODO:Move the update home screen to homescreen.py
class HomeScreenController:
"""
The `MainScreenController` class represents a controller implementation.
The `HomeScreenController` class represents a controller implementation.
Coordinates work of the view with the model.
The controller implements the strategy pattern. The controller connects to
the view to control its actions.
@@ -25,6 +25,7 @@ class HomeScreenController:
self.view = HomeScreenView(controller=self, model=self.model)
if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
Clock.schedule_once(lambda _:self.populate_home_screen())
def get_view(self) -> HomeScreenView:
return self.view
@@ -108,16 +109,13 @@ class HomeScreenController:
def populate_home_screen(self):
self.populate_errors = []
self.trending_anime()
self.highest_scored_anime()
self.popular_anime()
self.favourite_anime()
self.recently_updated_anime()
self.upcoming_anime()
Clock.schedule_once(lambda _:self.trending_anime())
Clock.schedule_once(lambda _:self.highest_scored_anime())
Clock.schedule_once(lambda _:self.popular_anime())
Clock.schedule_once(lambda _: self.favourite_anime())
Clock.schedule_once(lambda _:self.recently_updated_anime())
Clock.schedule_once(lambda _:self.upcoming_anime())
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)}")
def update_my_list(self,*args):
self.model.update_user_anime_list(*args)

View File

@@ -1,16 +1,16 @@
from inspect import isgenerator
from kivy.logger import Logger
from kivy.clock import Clock
# from kivy.clock import Clock
from kivy.utils import difference
from View import MyListScreenView
from Model import MyListScreenModel
from Utility import show_notification,user_data_helper
from Utility import user_data_helper
class MyListScreenController:
"""
The `MainScreenController` class represents a controller implementation.
The `MyListScreenController` class represents a controller implementation.
Coordinates work of the view with the model.
The controller implements the strategy pattern. The controller connects to
the view to control its actions.
@@ -20,6 +20,7 @@ class MyListScreenController:
self.model = model
self.view = MyListScreenView(controller=self, model=self.model)
self.requested_update_my_list_screen()
def get_view(self) -> MyListScreenView:
return self.view
@@ -33,5 +34,3 @@ class MyListScreenController:
for result_card in anime_cards:
result_card.screen = self.view
self.view.update_layout(result_card)

View File

@@ -8,6 +8,8 @@ from Model import SearchScreenModel
class SearchScreenController:
"""The search screen controller
"""
def __init__(self, model: SearchScreenModel):
self.model = model
@@ -17,6 +19,8 @@ class SearchScreenController:
return self.view
def update_trending_anime(self):
"""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()
@@ -37,7 +41,7 @@ class SearchScreenController:
Clock.schedule_once(
lambda _: self.view.update_pagination(self.model.pagination_info)
)
Clock.schedule_once(lambda _: self.update_trending_anime())
self.update_trending_anime()
else:
Logger.error(f"Home Screen:Failed to search for {anime_title}")
self.view.is_searching = False

View File

@@ -1,19 +1,12 @@
import json
import os
from Model.base_model import BaseScreenModel
from libs.anilist import AniList
from Utility.media_card_loader import MediaCardLoader
from kivy.storage.jsonstore import JsonStore
user_data= JsonStore("user_data.json")
class AnimeScreenModel(BaseScreenModel):
"""the Anime screen model
"""
data = {}
anime_id = 0
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
def get_anime_data(self,id:int):
return AniList.get_anime(id)

View File

@@ -5,16 +5,4 @@ class DownloadsScreenModel(BaseScreenModel):
"""
Handles the download screen logic
"""
# already_in_user_anime_list = []
# def update_my_anime_list_view(self,not_yet_in_user_anime_list:list,**kwargs):
# success,self.data = AniList.search(id_in=not_yet_in_user_anime_list)
# if success:
# return self.media_card_generator()
# else:
# show_notification(f"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)

View File

@@ -1,73 +1,79 @@
from Model.base_model import BaseScreenModel
from libs.anilist import AniList
from Utility.media_card_loader import MediaCardLoader
from kivy.storage.jsonstore import JsonStore
user_data= JsonStore("user_data.json")
class HomeScreenModel(BaseScreenModel):
"""The home screen model"""
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)
return _data_generator()
else:
return data
def get_most_favourite_anime(self):
success,data = AniList.get_most_favourite()
success, data = AniList.get_most_favourite()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_recently_updated_anime(self):
success,data = AniList.get_most_recently_updated()
success, data = AniList.get_most_recently_updated()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_popular_anime(self):
success,data = AniList.get_most_popular()
success, data = AniList.get_most_popular()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_most_scored_anime(self):
success,data = AniList.get_most_scored()
success, data = AniList.get_most_scored()
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def get_upcoming_anime(self):
success,data = AniList.get_upcoming_anime(1)
success, data = AniList.get_upcoming_anime(1)
if success:
def _data_generator():
for anime_item in data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
return _data_generator()
else:
return data
def update_user_anime_list(self,anime_id,is_add):
my_list:list = user_data.get("my_list")["list"]
if is_add:
my_list.append(anime_id)
elif not(is_add) and my_list:
my_list.remove(anime_id)
user_data.put("my_list",list=my_list)

View File

@@ -1,10 +1,6 @@
from typing import Generator
from Model.base_model import BaseScreenModel
from libs.anilist import AniList
from Utility.media_card_loader import MediaCardLoader
from View.components import MediaCard
from Utility import show_notification
from Utility import MediaCardLoader, show_notification
class SearchScreenModel(BaseScreenModel):
@@ -31,7 +27,3 @@ class SearchScreenModel(BaseScreenModel):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
self.pagination_info = self.data["data"]["Page"]["pageInfo"]
# def extract_pagination_info(self):
# pagination_info = None
# return pagination_info

View File

@@ -1,5 +1,9 @@
from datetime import datetime
from libs.anilist.anilist_data_schema import AnilistDateObject,AnilistMediaNextAiringEpisode
from libs.anilist.anilist_data_schema import (
AnilistDateObject,
AnilistMediaNextAiringEpisode,
)
# TODO: Add formating options for the final date
@@ -24,7 +28,7 @@ def format_list_data_with_comma(data: list | None):
return "None"
def extract_next_airing_episode(airing_episode:AnilistMediaNextAiringEpisode):
def extract_next_airing_episode(airing_episode: AnilistMediaNextAiringEpisode):
if airing_episode:
return f"Episode: {airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
else:

View File

@@ -1,6 +1,6 @@
"""
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']
# import time
# from datetime import date,datetime
# print(datetime.fromtimestamp(1716412399))
# print(time.daylight,date.max,date.min)
'Yellowgreen']

View File

@@ -8,38 +8,47 @@ from kivy.utils import get_hex_from_color
def bolden(text: str):
return f"[b]{text}[/b]"
def italicize(text: str):
return f"[i]{text}[/i]"
def underline(text: str):
return f"[u]{text}[/u]"
def strike_through(text: str):
return f"[s]{text}[/s]"
def sub_script(text: str):
return f"[sub]{text}[/sub]"
def super_script(text: str):
return f"[sup]{text}[/sup]"
def color_text(text: str, color: tuple):
hex_color = get_hex_from_color(color)
return f"[color={hex_color}]{text}[/color]"
def font(text: str, font_name: str):
return f"[font={font_name}]{text}[/font]"
def font_family(text: str, family: str):
return f"[font_family={family}]{text}[/font_family]"
def font_context(text: str, context: str):
return f"[font_context={context}]{text}[/font_context]"
def font_size(text: str, size: int):
return f"[size={size}]{text}[/size]"
def text_ref(text: str, ref: str):
return f"[ref={ref}]{text}[/ref]"

View File

@@ -26,6 +26,8 @@ for link in yt_stream_links:
# 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

View File

@@ -1,22 +1,29 @@
from kivymd.uix.snackbar import MDSnackbar,MDSnackbarText,MDSnackbarSupportingText
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText, MDSnackbarSupportingText
from kivy.clock import Clock
def show_notification(title,details):
def show_notification(title, details):
"""helper function to display notifications
Args:
title (str): the title of your message
details (str): the details of your message
"""
def _show(dt):
MDSnackbar(
MDSnackbarText(
text=title,
adaptive_height=True,
),
MDSnackbarSupportingText(
text=details,
shorten=False,
max_lines=0,
adaptive_height=True
text=details, shorten=False, max_lines=0, adaptive_height=True
),
duration=5,
y="10dp",
pos_hint={"bottom": 1,"right":.99},
pos_hint={"bottom": 1, "right": 0.99},
padding=[0, 0, "8dp", "8dp"],
size_hint_x=.4
size_hint_x=0.4,
).open()
Clock.schedule_once(_show,1)
Clock.schedule_once(_show, 1)

View File

@@ -2,9 +2,8 @@
Contains Helper functions to read and write the user data files
"""
from kivy.storage.jsonstore import JsonStore
from datetime import date,datetime
from datetime import date, datetime
from kivy.logger import Logger
today = date.today()
@@ -15,56 +14,68 @@ yt_cache = JsonStore("yt_cache.json")
# Get the user data
def get_user_anime_list()->list:
def get_user_anime_list() -> list:
try:
return user_data.get("user_anime_list")["user_anime_list"] # returns a list of anime ids
return user_data.get("user_anime_list")[
"user_anime_list"
] # returns a list of anime ids
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_user_anime_list(updated_list:list):
def update_user_anime_list(updated_list: list):
try:
updated_list_ = list(set(updated_list))
user_data.put("user_anime_list",user_anime_list=updated_list_)
user_data.put("user_anime_list", user_anime_list=updated_list_)
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")
# Get the user data
def get_user_downloads()->list:
def get_user_downloads() -> list:
try:
return user_data.get("user_downloads")["user_downloads"] # returns a list of anime ids
return user_data.get("user_downloads")[
"user_downloads"
] # returns a list of anime ids
except Exception as e:
Logger.warning(f"User Data:Read failure:{e}")
return []
def update_user_downloads(updated_list:list):
def update_user_downloads(updated_list: list):
try:
user_data.put("user_downloads",user_downloads=list(set(updated_list)))
user_data.put("user_downloads", user_downloads=list(set(updated_list)))
except Exception as e:
Logger.warning(f"User Data:Update failure:{e}")
# Yt persistent anime trailer cache
# Yt persistent anime trailer cache
t = 1
if now.hour<=6:
if now.hour <= 6:
t = 1
elif now.hour<=12:
elif now.hour <= 12:
t = 2
elif now.hour<=18:
elif now.hour <= 18:
t = 3
else:
t = 4
yt_anime_trailer_cache_name = f"{today}{t}"
def get_anime_trailer_cache()->list:
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):
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})
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,23 +1,26 @@
from datetime import datetime
# import tempfile
import shutil
# import os
# TODO: make it use color_text instead of fixed vals
# from .kivy_markup_helper import color_text
# utility functions
def write_crash(e:Exception):
def write_crash(e: Exception):
index = datetime.today()
error = f"[b][color=#fa0000][ {index} ]:[/color][/b]\n(\n\n{e}\n\n)\n"
try:
with open("crashdump.txt","a") as file:
with open("crashdump.txt", "a") as file:
file.write(error)
except:
with open("crashdump.txt","w") as file:
with open("crashdump.txt", "w") as file:
file.write(error)
return index
def move_file(source_path,dest_path):
def move_file(source_path, dest_path):
try:
path = shutil.move(source_path,dest_path)
return (1,path)
path = shutil.move(source_path, dest_path)
return (1, path)
except Exception as e:
return (0,e)
return (0, e)

View File

@@ -19,6 +19,7 @@ from .components import (
class AnimeScreenView(BaseScreenView):
"""The anime screen view"""
caller_screen_name = StringProperty()
header: AnimeHeader = ObjectProperty()
side_bar: AnimeSideBar = ObjectProperty()

View File

@@ -31,7 +31,7 @@
padding:"10dp"
orientation:"vertical"
StreamDialogHeaderLabel:
text:"Stream on Animdl"
text:"Stream Anime"
StreamDialogLabel:
text:"Title"
MDTextField:

View File

@@ -1,20 +1,35 @@
from kivy.clock import Clock
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
def __init__(self,data,mpv,**kwargs):
class AnimdlStreamDialog(
ThemableBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
"""The anime streaming dialog"""
def __init__(self, data, mpv, **kwargs):
super().__init__(**kwargs)
self.data = data
self.mpv=mpv
if title:=data["title"].get("romaji"):
self.mpv = mpv
if title := data["title"].get("romaji"):
self.ids.title_field.text = title
elif title:=data["title"].get("english"):
elif title := data["title"].get("english"):
self.ids.title_field.text = title
self.ids.quality_field.text = "best"
def stream_anime(self,app):
def _stream_anime(self, app):
if self.mpv:
streaming_cmds = {}
title = self.ids.title_field.text
@@ -27,7 +42,7 @@ class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavio
quality = self.ids.quality_field.text
if quality:
streaming_cmds["quality"] = quality
else:
else:
streaming_cmds["quality"] = "best"
app.watch_on_animdl(streaming_cmds)
@@ -38,14 +53,17 @@ class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavio
episodes_range = self.ids.range_field.text
if episodes_range:
cmds = [*cmds,"-r",episodes_range]
cmds = [*cmds, "-r", episodes_range]
latest = self.ids.latest_field.text
if latest:
cmds = [*cmds,"-s",latest]
cmds = [*cmds, "-s", latest]
quality = self.ids.quality_field.text
if quality:
cmds = [*cmds,"-q",quality]
cmds = [*cmds, "-q", quality]
app.watch_on_animdl(custom_options = cmds)
app.watch_on_animdl(custom_options=cmds)
def stream_anime(self, app):
Clock.schedule_once(lambda _: self._stream_anime(app))

View File

@@ -1,46 +1,56 @@
from kivy.properties import ObjectProperty,ListProperty
from kivy.clock import Clock
from kivy.properties import ObjectProperty, ListProperty
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeCharacter(MDBoxLayout):
voice_actors = ObjectProperty({
"name":"",
"image":""
})
character = ObjectProperty({
"name":"",
"gender":"",
"dateOfBirth":"",
"image":"",
"age":"",
"description":""
})
"""an Anime character data"""
voice_actors = ObjectProperty({"name": "", "image": ""})
character = ObjectProperty(
{
"name": "",
"gender": "",
"dateOfBirth": "",
"image": "",
"age": "",
"description": "",
}
)
class AnimeCharacters(MDBoxLayout):
"""The anime characters card"""
container = ObjectProperty()
characters = ListProperty()
def on_characters(self,instance,characters):
format_date = lambda date_: f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else ""
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)
for character_ in characters: # character (character,actor)
character = character_[0]
actors = character_[1]
anime_character = AnimeCharacter()
anime_character.character = {
"name":character["name"]["full"],
"gender":character["gender"],
"dateOfBirth":format_date(character["dateOfBirth"]),
"image":character["image"]["medium"],
"age":character["age"],
"description":character["description"]
"name": character["name"]["full"],
"gender": character["gender"],
"dateOfBirth": format_date(character["dateOfBirth"]),
"image": character["image"]["medium"],
"age": character["age"],
"description": character["description"],
}
anime_character.voice_actors = {
"name":", ".join([actor["name"]["full"] for actor in actors])
"name": ", ".join([actor["name"]["full"] for actor in actors])
}
# anime_character.voice_actor =
self.container.add_widget(anime_character)
def on_characters(self, *args):
Clock.schedule_once(lambda _: self.update_characters_card(*args))

View File

@@ -3,6 +3,7 @@
padding:"10dp"
spacing:"10dp"
pos_hint: {'center_x': 0.5}
# StackLayout:
MDButton:
on_press:
root.screen.add_to_user_anime_list()

View File

@@ -4,4 +4,6 @@ from kivymd.uix.boxlayout import MDBoxLayout
class Controls(MDBoxLayout):
"""The diferent controls available"""
screen = ObjectProperty()

View File

@@ -1,16 +1,3 @@
# <DescriptionHeader>
# adaptive_height:True
# md_bg_color:self.theme_cls.secondaryContainerColor
# MDLabel:
# text:root.text
# adaptive_height:True
# max_lines:0
# shorten:False
# bold:True
# font_style: "Body"
# role: "large"
# padding:"10dp"
<DescriptionContainer@MDBoxLayout>:
adaptive_height:True
md_bg_color:self.theme_cls.surfaceContainerLowColor

View File

@@ -4,5 +4,6 @@ from kivymd.uix.boxlayout import MDBoxLayout
class AnimeDescription(MDBoxLayout):
description = StringProperty()
"""The anime description"""
description = StringProperty()

View File

@@ -1,26 +1,41 @@
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior
from kivymd.uix.behaviors import (
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
)
from kivymd.theming import ThemableBehavior
# from main import AniXStreamApp
class DownloadAnimeDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
def __init__(self,data,**kwargs):
super(DownloadAnimeDialog,self).__init__(**kwargs)
class DownloadAnimeDialog(
ThemableBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
"""The download anime dialog"""
def __init__(self, data, **kwargs):
super(DownloadAnimeDialog, self).__init__(**kwargs)
self.data = data
self.anime_id = self.data["id"]
if title:=data["title"].get("romaji"):
if title := data["title"].get("romaji"):
self.ids.title_field.text = title
elif title:=data["title"].get("english"):
elif title := data["title"].get("english"):
self.ids.title_field.text = title
self.ids.quality_field.text = "best"
def download_anime(self,app):
def download_anime(self, app):
default_cmds = {}
title=self.ids.title_field.text
title = self.ids.title_field.text
default_cmds["title"] = title
if episodes_range:=self.ids.range_field.text:
if episodes_range := self.ids.range_field.text:
default_cmds["episodes_range"] = episodes_range
if quality:=self.ids.range_field.text:
if quality := self.ids.range_field.text:
default_cmds["quality"] = quality
# print(title,episodes_range,latest,quality)
app.download_anime(self.anime_id,default_cmds)
app.download_anime(self.anime_id, default_cmds)

View File

@@ -1,7 +1,6 @@
<AnimeHeader>:
adaptive_height:True
orientation: 'vertical'
# padding:"10dp"
MDBoxLayout:
adaptive_height:True
md_bg_color:self.theme_cls.secondaryContainerColor

View File

@@ -6,4 +6,3 @@ from kivymd.uix.boxlayout import MDBoxLayout
class AnimeHeader(MDBoxLayout):
titles = StringProperty()
banner_image = StringProperty()

View File

@@ -6,9 +6,8 @@ from kivymd.uix.boxlayout import MDBoxLayout
class RankingsBar(MDBoxLayout):
rankings = DictProperty(
{
"Popularity":0,
"Favourites":0,
"AverageScore":0,
"Popularity": 0,
"Favourites": 0,
"AverageScore": 0,
}
)

View File

@@ -1,26 +1,29 @@
from kivy.properties import ObjectProperty,ListProperty
from kivy.properties import ObjectProperty, ListProperty
from kivy.clock import Clock
from kivymd.uix.boxlayout import MDBoxLayout
class AnimeReview(MDBoxLayout):
review = ObjectProperty({
"username":"",
"avatar":"",
"summary":""
})
review = ObjectProperty({"username": "", "avatar": "", "summary": ""})
class AnimeReviews(MDBoxLayout):
"""anime reviews"""
reviews = ListProperty()
container = ObjectProperty()
def on_reviews(self,instance,reviews):
def on_reviews(self, *args):
Clock.schedule_once(lambda _: self.update_reviews_card(*args))
def update_reviews_card(self, instance, reviews):
self.container.clear_widgets()
for review in reviews:
review_ = AnimeReview()
review_.review = {
"username":review["user"]["name"],
"avatar":review["user"]["avatar"]["medium"],
"summary":review["summary"]
"username": review["user"]["name"],
"avatar": review["user"]["avatar"]["medium"],
"summary": review["summary"],
}
self.container.add_widget(review_)

View File

@@ -7,8 +7,6 @@
spacing:"10dp"
orientation: 'vertical'
pos_hint: {'center_x': 0.5}
<SideBarLabel>:
adaptive_height:True
max_lines:0

View File

@@ -1,4 +1,4 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty,ListProperty
from kivy.properties import ObjectProperty, StringProperty, DictProperty, ListProperty
from kivy.utils import get_hex_from_color
from kivy.factory import Factory
@@ -10,34 +10,42 @@ class HeaderLabel(MDBoxLayout):
text = StringProperty()
halign = StringProperty("center")
Factory.register("HeaderLabel", HeaderLabel)
class SideBarLabel(MDLabel):
pass
# TODO:Switch to using the kivy_markup_module
class AnimeSideBar(MDBoxLayout):
screen = ObjectProperty()
image = StringProperty()
alternative_titles = DictProperty({
"synonyms":"",
"english":"",
"japanese":"",
})
information = DictProperty({
"episodes":"",
"status":"",
"aired":"",
"nextAiringEpisode":"",
"premiered":"",
"broadcast":"",
"countryOfOrigin":"",
"hashtag":"",
"studios":"", # { "name": "Sunrise", "isAnimationStudio": true }
"source":"",
"genres":"",
"duration":"",
"producers":"",
})
alternative_titles = DictProperty(
{
"synonyms": "",
"english": "",
"japanese": "",
}
)
information = DictProperty(
{
"episodes": "",
"status": "",
"aired": "",
"nextAiringEpisode": "",
"premiered": "",
"broadcast": "",
"countryOfOrigin": "",
"hashtag": "",
"studios": "", # { "name": "Sunrise", "isAnimationStudio": true }
"source": "",
"genres": "",
"duration": "",
"producers": "",
}
)
statistics = ListProperty()
statistics_container = ObjectProperty()
external_links = ListProperty()
@@ -45,7 +53,7 @@ class AnimeSideBar(MDBoxLayout):
tags = ListProperty()
tags_container = ObjectProperty()
def on_statistics(self,instance,value):
def on_statistics(self, instance, value):
self.statistics_container.clear_widgets()
header = HeaderLabel()
header.text = "Rankings"
@@ -56,10 +64,11 @@ class AnimeSideBar(MDBoxLayout):
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
stat[0].capitalize(),
f"{stat[1]}")
f"{stat[1]}",
)
self.statistics_container.add_widget(label)
def on_tags(self,instance,value):
def on_tags(self, instance, value):
self.tags_container.clear_widgets()
header = HeaderLabel()
header.text = "Tags"
@@ -69,11 +78,11 @@ class AnimeSideBar(MDBoxLayout):
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
tag[0].capitalize(),
f"{tag[1]} %")
f"{tag[1]} %",
)
self.tags_container.add_widget(label)
def on_external_links(self,instance,value):
def on_external_links(self, instance, value):
self.external_links_container.clear_widgets()
header = HeaderLabel()
header.text = "External Links"
@@ -84,5 +93,6 @@ class AnimeSideBar(MDBoxLayout):
label.text = "[color={}]{}:[/color] {}".format(
get_hex_from_color(label.theme_cls.primaryColor),
site[0].capitalize(),
site[1])
site[1],
)
self.external_links_container.add_widget(label)

View File

@@ -3,6 +3,7 @@ from View.base_screen import BaseScreenView
class CrashLogScreenView(BaseScreenView):
"""The crash log screen"""
main_container = ObjectProperty()
def model_is_changed(self) -> None:
"""

View File

@@ -25,4 +25,4 @@
theme_text_color:"Secondary"
text:color_text(root.episodes_to_download,root.theme_cls.secondaryColor)
MDIcon:
icon:"check-bold"
icon:"download"

View File

@@ -4,29 +4,29 @@ from kivy.logger import Logger
from kivy.utils import format_bytes_to_human
from View.base_screen import BaseScreenView
from .components.task_card import TaskCard
from .components.task_card import TaskCard
class DownloadsScreenView(BaseScreenView):
main_container = ObjectProperty()
progress_bar = ObjectProperty()
download_progress_label = ObjectProperty()
def on_new_download_task(self,anime_title:str,episodes:str|None):
def on_new_download_task(self, anime_title: str, episodes: str | None):
if not episodes:
episodes = "All"
self.main_container.add_widget(TaskCard(anime_title,episodes))
Clock.schedule_once(
lambda _: self.main_container.add_widget(TaskCard(anime_title, episodes))
)
def on_episode_download_progress(self,current_bytes_downloaded,total_bytes,episode_info):
percentage_completion = round((current_bytes_downloaded/total_bytes)*100)
def on_episode_download_progress(
self, current_bytes_downloaded, total_bytes, episode_info
):
percentage_completion = round((current_bytes_downloaded / total_bytes) * 100)
progress_text = f"Downloading: {episode_info['anime_title']} - {episode_info['episode']} ({format_bytes_to_human(current_bytes_downloaded)}/{format_bytes_to_human(total_bytes)})"
if (percentage_completion%5)==0:
self.progress_bar.value= max(min(percentage_completion,100),0)
if (percentage_completion % 5) == 0:
self.progress_bar.value = max(min(percentage_completion, 100), 0)
self.download_progress_label.text = progress_text
Logger.info(f"Downloader: {progress_text}")
# def on_enter(self):
# Clock.schedule_once(lambda _:self.controller.requested_update_my_list_screen())
def update_layout(self,widget):
def update_layout(self, widget):
self.user_anime_list_container.add_widget(widget)

View File

@@ -1,8 +1,6 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<HelpCard@MDBoxLayout>
spacing:"10dp"
orientation:"vertical"

View File

@@ -4,11 +4,13 @@ from View.base_screen import BaseScreenView
from Utility.kivy_markup_helper import bolden, color_text, underline
from Utility.data import themes_available
class HelpScreenView(BaseScreenView):
main_container = ObjectProperty()
animdl_help = StringProperty()
installing_animdl_help = StringProperty()
available_themes = StringProperty()
def __init__(self, **kw):
super(HelpScreenView, self).__init__(**kw)
self.animdl_help = f"""

View File

@@ -1,4 +1,5 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView

View File

@@ -1,20 +1,21 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty
from kivy.properties import ObjectProperty, StringProperty, DictProperty
from kivy.clock import Clock
from View.base_screen import BaseScreenView
class MyListScreenView(BaseScreenView):
user_anime_list_container = ObjectProperty()
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""
def on_enter(self):
Clock.schedule_once(lambda _:self.controller.requested_update_my_list_screen())
def update_layout(self,widget):
self.user_anime_list_container.add_widget(widget)
def on_enter(self):
Clock.schedule_once(lambda _: self.controller.requested_update_my_list_screen())
def update_layout(self, widget):
self.user_anime_list_container.add_widget(widget)

View File

@@ -58,7 +58,6 @@ class Filters(MDBoxLayout):
"NOT_YET_RELEASED",
"CANCELLED",
"HIATUS",
]
case _:
items = []

View File

@@ -2,7 +2,7 @@ from kivy.properties import ObjectProperty, StringProperty
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.navigationrail import MDNavigationRail
from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDIconButton
from kivymd.uix.tooltip import MDTooltip
@@ -26,6 +26,11 @@ class TooltipMDIconButton(Tooltip, MDIconButton):
tooltip_text = StringProperty()
class CommonNavigationRailItem(MDNavigationRailItem):
icon = StringProperty()
text = StringProperty()
class BaseScreenView(MDScreen, Observer):
"""
A base class that implements a visual representation of the model data.

View File

@@ -1,3 +1,3 @@
# <MDLabel>:
# allow_copy:True
# allow_selection:True
<MDLabel>:
allow_copy:True
allow_selection:True

View File

@@ -4,18 +4,29 @@ from kivy.animation import Animation
from kivy.uix.modalview import ModalView
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import BackgroundColorBehavior,StencilBehavior,CommonElevationBehavior,HoverBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
StencilBehavior,
CommonElevationBehavior,
HoverBehavior,
)
class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
class MediaPopup(
ThemableBehavior,
HoverBehavior,
StencilBehavior,
CommonElevationBehavior,
BackgroundColorBehavior,
ModalView,
):
caller = ObjectProperty()
player = ObjectProperty()
def __init__(self, caller,*args,**kwarg):
def __init__(self, caller, *args, **kwarg):
self.caller = caller
super(MediaPopup,self).__init__(*args,**kwarg)
super(MediaPopup, self).__init__(*args, **kwarg)
def open(self, *_args, **kwargs):
"""Display the modal in the Window.
@@ -26,33 +37,32 @@ class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationB
"""
from kivy.core.window import Window
if self._is_open:
return
self._window = Window
self._is_open = True
self.dispatch('on_pre_open')
self.dispatch("on_pre_open")
Window.add_widget(self)
Window.bind(
on_resize=self._align_center,
on_keyboard=self._handle_keyboard)
Window.bind(on_resize=self._align_center, on_keyboard=self._handle_keyboard)
self.center = self.caller.to_window(*self.caller.center)
self.fbind('center', self._align_center)
self.fbind('size', self._align_center)
if kwargs.get('animation', True):
ani = Animation(_anim_alpha=1., d=self._anim_duration)
ani.bind(on_complete=lambda *_args: self.dispatch('on_open'))
self.fbind("center", self._align_center)
self.fbind("size", self._align_center)
if kwargs.get("animation", True):
ani = Animation(_anim_alpha=1.0, d=self._anim_duration)
ani.bind(on_complete=lambda *_args: self.dispatch("on_open"))
ani.start(self)
else:
self._anim_alpha = 1.
self.dispatch('on_open')
self._anim_alpha = 1.0
self.dispatch("on_open")
def _align_center(self, *_args):
if self._is_open:
self.center = self.caller.to_window(*self.caller.center)
def on_leave(self,*args):
def on_leave(self, *args):
def _leave(dt):
if not self.hovering:
self.dismiss()
Clock.schedule_once(_leave,2)
Clock.schedule_once(_leave, 2)

View File

@@ -1,8 +1,3 @@
<CommonNavigationRailItem@MDNavigationRailItem>
icon:""
text:""
<CommonNavigationRailItem>
MDNavigationRailItemIcon:
icon:root.icon

View File

@@ -1,12 +1,10 @@
import os
import time
import json
import re
import shutil
from subprocess import Popen, run, PIPE, CompletedProcess
from typing import Callable
from .extras import Logger
from .animdl_data_helper import (
filter_broken_streams,
@@ -23,7 +21,7 @@ from .animdl_exceptions import (
NoValidAnimeStreamsException,
Python310NotFoundException,
)
from .animdl_types import AnimdlAnimeUrlAndTitle, AnimdlData
from .animdl_types import AnimdlAnimeEpisode, AnimdlAnimeUrlAndTitle, AnimdlData
broken_link_pattern = r"https://tools.fast4speed.rsvp/\w*"
@@ -129,12 +127,25 @@ class AnimdlApi:
)
return most_likely_anime_url_and_title # ("title","anime url")
else:
raise AnimdlAnimeUrlNotFoundException
raise AnimdlAnimeUrlNotFoundException(
"The anime your searching for doesnt exist or animdl is broken or not in your system path"
)
@classmethod
def stream_anime_by_title_on_animdl(
cls, title, episodes_range=None, quality: str = "best"
cls, title: str, episodes_range: str | None = None, quality: str = "best"
) -> Popen:
"""Streams the anime title on animdl
Args:
title (str): the anime title you want to stream
episodes_range (str, optional): the episodes you want to stream; should be a valid animdl range. Defaults to None.
quality (str, optional): the quality of the stream. Defaults to "best".
Returns:
Popen: the stream child subprocess for mor control
"""
anime = cls.get_anime_url_by_title(title)
base_cmds = ["stream", anime[1], "-q", quality]
@@ -145,6 +156,17 @@ class AnimdlApi:
def stream_anime_with_mpv(
cls, title: str, episodes_range: str | None = None, quality: str = "best"
):
"""Stream an anime directly with mpv without having to interact with animdl cli
Args:
title (str): the anime title you want to stream
episodes_range (str | None, optional): a valid animdl episodes range you want ito watch. Defaults to None.
quality (str, optional): the quality of the stream. Defaults to "best".
Yields:
Popen: the child subprocess you currently are watching
"""
anime_data = cls.get_all_stream_urls_by_anime_title(title, episodes_range)
stream = []
for episode in anime_data.episodes:
@@ -186,8 +208,18 @@ class AnimdlApi:
@classmethod
def get_all_anime_stream_urls_by_anime_url(
cls, anime_url: str, episodes_range=None
):
cls, anime_url: str, episodes_range: str | None = None
) -> list[AnimdlAnimeEpisode]:
"""gets all the streams for the animdl url
Args:
anime_url (str): an animdl url used in scraping
episodes_range (str | None, optional): a valid animdl episodes range. Defaults to None.
Returns:
list[AnimdlAnimeEpisode]: A list of anime episodes gotten from animdl
"""
cmd = (
["grab", anime_url, "-r", episodes_range]
if episodes_range
@@ -207,8 +239,9 @@ class AnimdlApi:
episodes_range (str, optional): an animdl episodes range. Defaults to None.
Returns:
_type_: _description_
AnimdlData: The parsed data from animdl grab
"""
possible_anime = cls.get_anime_url_by_title(title)
return AnimdlData(
possible_anime.anime_title,
@@ -235,6 +268,23 @@ class AnimdlApi:
episodes_range: str | None = None,
quality: str = "best",
) -> tuple[list[int], list[int]]:
"""Downloads anime either adaptive, progressive, or .m3u streams and uses mpv to achieve this
Args:
_anime_title (str): the anime title you want to download
on_episode_download_progress (Callable): the callback when a chunk of an episode is downloaded
on_episode_download_complete (Callable): the callback when an episode has been successfully downloaded
on_complete (Callable): callback when the downloading process is complete
output_path (str): the directory | folder to download the anime
episodes_range (str | None, optional): a valid animdl episode range. Defaults to None.
quality (str, optional): the anime quality. Defaults to "best".
Raises:
NoValidAnimeStreamsException: raised when no valid streams were found for a particular episode
Returns:
tuple[list[int], list[int]]: a tuple containing successful, and failed downloads list
"""
anime_streams_data = cls.get_all_stream_urls_by_anime_title(
_anime_title, episodes_range
@@ -260,6 +310,11 @@ class AnimdlApi:
streams = filter_broken_streams(episode["streams"])
# raises an exception if no streams for current episodes
if not streams:
raise NoValidAnimeStreamsException(
f"No valid streams were found for episode {episode_number}"
)
episode_stream = filter_streams_by_quality(streams, quality)
# determine episode_title
@@ -340,6 +395,17 @@ class AnimdlApi:
@classmethod
def download_with_mpv(cls, url: str, output_path: str, on_progress: Callable):
"""The method used to download a remote resource with mpv
Args:
url (str): the url of the remote resource to download
output_path (str): the location to download the resource to
on_progress (Callable): the callback when a chunk of the resource is downloaded
Returns:
subprocess return code: the return code of the mpv subprocess
"""
mpv_child_process = run_mpv_command(url, f"--stream-dump={output_path}")
progress_regex = re.compile(r"\d+/\d+") # eg Dumping 2044776/125359745
@@ -361,6 +427,18 @@ class AnimdlApi:
episode_info: dict[str, str],
on_progress: Callable,
):
"""the progressive downloader of mpv
Args:
video_url (str): a video url
output_path (str): download location
episode_info (dict[str, str]): the details of the episode we downloading
on_progress (Callable): the callback when a chunk is downloaded
Raises:
Exception: exception raised when anything goes wrong
"""
episode = (
path_parser(episode_info["anime_title"])
+ " - "
@@ -385,6 +463,20 @@ class AnimdlApi:
on_progress: Callable,
episode_info: dict[str, str],
):
"""the adaptive downloader
Args:
video_url (str): url of video you want ot download
audio_url (str): url of audio file you want ot download
sub_url (str): url of sub file you want ot download
output_path (str): download location
on_progress (Callable): the callback when a chunk is downloaded
episode_info (dict[str, str]): episode details
Raises:
Exception: incase anything goes wrong
"""
on_progress_ = lambda current_bytes, total_bytes: on_progress(
current_bytes, total_bytes, episode_info
)
@@ -421,6 +513,19 @@ class AnimdlApi:
on_progress: Callable,
episode_info: dict[str, str],
):
"""only downloads video and subs
Args:
video_url (str): url of video you want ot download
sub_url (str): url of sub you want ot download
output_path (str): the download location
on_progress (Callable): the callback for when a chunk is downloaded
episode_info (dict[str, str]): episode details
Raises:
Exception: when anything goes wrong
"""
on_progress_ = lambda current_bytes, total_bytes: on_progress(
current_bytes, total_bytes, episode_info
)
@@ -441,21 +546,3 @@ class AnimdlApi:
if is_video_failure:
raise Exception
# TODO: ADD RUN_MPV_COMMAND = RAISES MPV NOT FOR ND EXCEPTION
# TODO: ADD STREAM WITH MPV
if __name__ == "__main__":
title = input("enter title: ")
e_range = input("enter range: ")
start = time.time()
# t = AnimdlApi.download_anime_by_title(
# title, lambda *u: print(u), lambda *u: print(u)
# ,lambda *u:print(u),".",episodes_range=e_range)
streamer = AnimdlApi.stream_anime_with_mpv(title, e_range, quality="720")
# with open("test.json","w") as file:
# print(json.dump(t,file))
for stream in streamer:
print(stream.communicate())
delta = time.time() - start
print(f"Took: {delta} secs")

View File

@@ -4,7 +4,11 @@ import json
from fuzzywuzzy import fuzz
from .extras import Logger
from .animdl_types import AnimdlAnimeUrlAndTitle,AnimdlData,AnimdlAnimeEpisode,AnimdlEpisodeStream
from .animdl_types import (
AnimdlAnimeUrlAndTitle,
AnimdlAnimeEpisode,
AnimdlEpisodeStream,
)
# Currently this links don't work so we filter it out
@@ -70,6 +74,15 @@ def anime_title_percentage_match(
def filter_broken_streams(
streams: list[AnimdlEpisodeStream],
) -> list[AnimdlEpisodeStream]:
"""filters the streams that the project has evaluated doesnt work
Args:
streams (list[AnimdlEpisodeStream]): the streams to filter
Returns:
list[AnimdlEpisodeStream]: the valid streams
"""
stream_filter = lambda stream: (
True if not re.match(broken_link_pattern, stream["stream_url"]) else False
)
@@ -77,9 +90,19 @@ def filter_broken_streams(
def filter_streams_by_quality(
anime_episode_streams: list[AnimdlEpisodeStream], quality: str|int, strict=False
anime_episode_streams: list[AnimdlEpisodeStream], quality: str | int, strict=False
) -> AnimdlEpisodeStream:
# filtered_streams = []
"""filters streams by quality
Args:
anime_episode_streams (list[AnimdlEpisodeStream]): the streams to filter
quality (str | int): the quality you want to get
strict (bool, optional): whether to always return an episode if quality isn,t found. Defaults to False.
Returns:
AnimdlEpisodeStream: the stream of specified quality
"""
# get the appropriate stream or default to best
get_quality_func = lambda stream_: (
stream_.get("quality") if stream_.get("quality") else 0
@@ -104,8 +127,16 @@ def filter_streams_by_quality(
# return AnimdlEpisodeStream({})
# TODO: add typing to return dict
def parse_stream_urls_data(raw_stream_urls_data: str) -> list[AnimdlAnimeEpisode]:
"""parses the streams data gotten from animdl grab
Args:
raw_stream_urls_data (str): the animdl grab data to parse
Returns:
list[AnimdlAnimeEpisode]: the parsed streams for all episode
"""
try:
return [
AnimdlAnimeEpisode(json.loads(episode.strip()))
@@ -123,8 +154,9 @@ def search_output_parser(raw_data: str) -> list[AnimdlAnimeUrlAndTitle]:
raw_data (str): valid animdl data
Returns:
dict: parsed animdl data containing an anime title
AnimdlAnimeUrlAndTitle: parsed animdl data containing an animdl anime url and anime title
"""
# get each line of dat and ignore those that contain unwanted data
data = raw_data.split("\n")[3:]

View File

@@ -1 +0,0 @@
["jujutsu kaisen", [{"episode": 1, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/1"}]}, {"episode": 2, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/2"}]}, {"episode": 3, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/3"}]}]]

View File

@@ -42,7 +42,6 @@ if not (user_data_helper.yt_cache.exists("yt_stream_links")):
# TODO: Confirm data integrity from user_data and yt_cache
# TODO: Arrange the app methods
class AniXStreamApp(MDApp):
queue = Queue()
downloads_queue = Queue()
@@ -51,13 +50,19 @@ class AniXStreamApp(MDApp):
def worker(self, queue: Queue):
while True:
task = queue.get() # task should be a function
task()
try:
task()
except Exception as e:
show_notification("An error occured while streaming",f"{e}")
self.queue.task_done()
def downloads_worker(self, queue: Queue):
while True:
download_task = queue.get() # task should be a function
download_task()
try:
download_task()
except Exception as e:
show_notification("An error occured while downloading",f"{e}")
self.downloads_queue.task_done()
def __init__(self, **kwargs):
@@ -111,12 +116,20 @@ class AniXStreamApp(MDApp):
self.manager_screens.add_widget(view)
def build_config(self, config):
if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore
downloads_dir = os.path.join(vid_path, "anixstream")
if not os.path.exists(downloads_dir):
os.mkdir(downloads_dir)
else:
downloads_dir = os.path.join(".", "videos")
if not os.path.exists(downloads_dir):
os.mkdir(downloads_dir)
config.setdefaults(
"Preferences",
{
"theme_color": "Cyan",
"theme_style": "Dark",
"downloads_dir": plyer.storagepath.get_videos_dir() if plyer.storagepath.get_videos_dir() else ".", # type: ignore
"downloads_dir": downloads_dir,
"is_startup_anime_enable": False,
},
)
@@ -204,6 +217,9 @@ class AniXStreamApp(MDApp):
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,
@@ -216,7 +232,9 @@ class AniXStreamApp(MDApp):
download_task = lambda: AnimdlApi.download_anime_by_title(
default_cmds["title"],
on_progress,
lambda *arg:print(arg),
lambda anime_title, episode: show_notification(
"Finished installing an episode", f"{anime_title}-{episode}"
),
self.download_anime_complete,
output_path,
) # ,default_cmds.get("quality")
@@ -271,20 +289,20 @@ class AniXStreamApp(MDApp):
)
def stream_anime_with_mpv(
self, title, episodes_range: str | None = None,quality:str="best"
self, title, episodes_range: str | None = None, quality: str = "best"
):
self.stop_streaming = False
streams = AnimdlApi.stream_anime_with_mpv(title,episodes_range,quality)
# TODO: End mpv child process properly
streams = AnimdlApi.stream_anime_with_mpv(title, episodes_range, quality)
# TODO: End mpv child process properly
for stream in streams:
self.animdl_streaming_subprocess= stream
for line in self.animdl_streaming_subprocess.stderr: # type: ignore
self.animdl_streaming_subprocess = stream
for line in self.animdl_streaming_subprocess.stderr: # type: ignore
if self.stop_streaming:
if stream:
stream.terminate()
stream.kill()
del stream
return
return
def watch_on_animdl(
self,
@@ -306,20 +324,24 @@ class AniXStreamApp(MDApp):
self.animdl_streaming_subprocess.kill()
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"]
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']}")
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
)
self.queue.put(stream_func)
show_notification("Streamer","Started streaming")
show_notification("Streamer", "Started streaming")
if __name__ == "__main__":
AniXStreamApp().run()

View File

@@ -4,4 +4,6 @@ ffpyplayer
plyer
https://github.com/kivymd/KivyMD/archive/master.zip
fuzzywuzzy
python-Levenshtein
python-Levenshtein
pyyaml
animdl

View File

@@ -1 +1 @@
{"user_anime_list": {"user_anime_list": [166531, 98437, 269, 104462, 21519, 150672, 104463, 21, 20631, 9756, 115230, 124194, 9253, 6702, 4654, 122671, 20657, 16049, 125367, 6213, 100185, 111322, 107226, 15583, 21857, 97889, 166372, 21745, 104051, 5114, 151806]}}
{"user_anime_list": {"user_anime_list": [166531, 98437, 269, 104462, 21519, 150672, 104463, 21, 20631, 9756, 115230, 124194, 9253, 6702, 4654, 122671, 16049, 125367, 6213, 100185, 111322, 107226, 5081, 15583, 21857, 97889, 166372, 21745, 104051, 5114, 151806]}}