completed the basic ui and anilist module

This commit is contained in:
Benedict Xavier Wanyonyi
2024-05-15 20:33:20 +03:00
commit ffe5db9e33
44 changed files with 3775 additions and 0 deletions

1
app/.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

View File

@@ -0,0 +1,3 @@
from .main_screen import MainScreenController
from .search_screen import SearchScreenController
from .my_list_screen import MyListScreenController

View File

@@ -0,0 +1,95 @@
from inspect import isgenerator
from View.MainScreen.main_screen import MainScreenView
from Model.main_screen import MainScreenModel
from View.components.media_card.media_card import MediaCardsContainer
from Utility import show_notification
class MainScreenController:
"""
The `MainScreenController` 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.
"""
def __init__(self, model:MainScreenModel):
self.model = model # Model.main_screen.MainScreenModel
self.view = MainScreenView(controller=self, model=self.model)
self.populate_home_screen()
def get_view(self) -> MainScreenView:
return self.view
def populate_home_screen(self):
errors = []
most_popular_cards_container = MediaCardsContainer()
most_popular_cards_container.list_name = "Most Popular"
most_popular_cards_generator = self.model.get_most_popular_anime()
if isgenerator(most_popular_cards_generator):
for card in most_popular_cards_generator:
card.screen = self.view
most_popular_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_popular_cards_container)
else:
errors.append("Most Popular Anime")
most_favourite_cards_container = MediaCardsContainer()
most_favourite_cards_container.list_name = "Most Favourites"
most_favourite_cards_generator = self.model.get_most_favourite_anime()
if isgenerator(most_favourite_cards_generator):
for card in most_favourite_cards_generator:
card.screen = self.view
most_favourite_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_favourite_cards_container)
else:
errors.append("Most favourite Anime")
trending_cards_container = MediaCardsContainer()
trending_cards_container.list_name = "Trending"
trending_cards_generator = self.model.get_trending_anime()
if isgenerator(trending_cards_generator):
for card in trending_cards_generator:
card.screen = self.view
trending_cards_container.container.add_widget(card)
self.view.main_container.add_widget(trending_cards_container)
else:
errors.append("trending Anime")
most_scored_cards_container = MediaCardsContainer()
most_scored_cards_container.list_name = "Most Scored"
most_scored_cards_generator = self.model.get_most_scored_anime()
if isgenerator(most_scored_cards_generator):
for card in most_scored_cards_generator:
card.screen = self.view
most_scored_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_scored_cards_container)
else:
errors.append("Most scored Anime")
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()
if isgenerator(most_recently_updated_cards_generator):
for card in most_recently_updated_cards_generator:
card.screen = self.view
most_recently_updated_cards_container.container.add_widget(card)
self.view.main_container.add_widget(most_recently_updated_cards_container)
else:
errors.append("Most recently updated Anime")
upcoming_cards_container = MediaCardsContainer()
upcoming_cards_container.list_name = "Upcoming Anime"
upcoming_cards_generator = self.model.get_upcoming_anime()
if isgenerator(upcoming_cards_generator):
for card in upcoming_cards_generator:
card.screen = self.view
upcoming_cards_container.container.add_widget(card)
self.view.main_container.add_widget(upcoming_cards_container)
else:
errors.append("upcoming Anime")
if errors:
show_notification(f"Failed to get the following {', '.join(errors)}","Theres probably a problem with your internet connection or anilist servers are down")
def update_my_list(self,*args):
self.model.update_user_anime_list(*args)

View File

@@ -0,0 +1,21 @@
from inspect import isgenerator
from View import MyListScreenView
from Model import MyListScreenModel
from View.components.media_card.media_card import MediaCardsContainer
from Utility import show_notification
class MyListScreenController:
"""
The `MainScreenController` 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.
"""
def __init__(self, model:MyListScreenModel):
self.model = model # Model.main_screen.MyListScreenModel
self.view = MyListScreenView(controller=self, model=self.model)
# self.populate_home_screen()
def get_view(self) -> MyListScreenView:
return self.view

View File

@@ -0,0 +1,23 @@
from inspect import isgenerator
from View import SearchScreenView
from Model import SearchScreenModel
class SearchScreenController:
def __init__(self, model:SearchScreenModel):
self.model = model # Model.main_screen.MainScreenModel
self.view = SearchScreenView(controller=self, model=self.model)
def get_view(self) -> SearchScreenView:
return self.view
def requested_search_for_anime(self,anime_title,**kwargs):
self.view.is_searching = True
data = self.model.search_for_anime(anime_title,**kwargs)
if isgenerator(data):
for result_card in data:
self.view.update_layout(result_card)
else:
print(data)
# self.view.add_pagination()
self.view.is_searching = False

3
app/Model/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .main_screen import MainScreenModel
from .search_screen import SearchScreenModel
from .my_list_screen import MyListScreenModel

33
app/Model/base_model.py Normal file
View File

@@ -0,0 +1,33 @@
# The model implements the observer pattern. This means that the class must
# support adding, removing, and alerting observers. In this case, the model is
# completely independent of controllers and views. It is important that all
# registered observers implement a specific method that will be called by the
# model when they are notified (in this case, it is the `model_is_changed`
# method). For this, observers must be descendants of an abstract class,
# inheriting which, the `model_is_changed` method must be overridden.
class BaseScreenModel:
"""Implements a base class for model modules."""
_observers = []
def add_observer(self, observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer) -> None:
self._observers.remove(observer)
def notify_observers(self, name_screen: str) -> None:
"""
Method that will be called by the observer when the model data changes.
:param name_screen:
name of the view for which the method should be called
:meth:`model_is_changed`.
"""
for observer in self._observers:
if observer.name == name_screen:
observer.model_is_changed()
break

73
app/Model/main_screen.py Normal file
View File

@@ -0,0 +1,73 @@
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 MainScreenModel(BaseScreenModel):
def get_trending_anime(self):
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()
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()
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()
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()
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)
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

@@ -0,0 +1,25 @@
import os
from Model.base_model import BaseScreenModel
from Utility import show_notification
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 MyListScreenModel(BaseScreenModel):
data = {}
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["Error"])
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
self.pagination_info = self.extract_pagination_info()
def extract_pagination_info(self):
pagination_info = None
return pagination_info

View File

@@ -0,0 +1,25 @@
import os
from Model.base_model import BaseScreenModel
from Utility import show_notification
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 SearchScreenModel(BaseScreenModel):
data = {}
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["Error"])
def media_card_generator(self):
for anime_item in self.data["data"]["Page"]["media"]:
yield MediaCardLoader.media_card(anime_item)
self.pagination_info = self.extract_pagination_info()
def extract_pagination_info(self):
pagination_info = None
return pagination_info

2
app/Utility/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .media_card_loader import MediaCardLoader
from .show_notification import show_notification

View File

@@ -0,0 +1,256 @@
from html.parser import HTMLParser
import os
from kivy.storage.jsonstore import JsonStore
from collections import deque
import threading
from time import sleep
from View.components import MediaCard
from pytube import YouTube
from kivy.loader import _ThreadPool
from kivy.clock import Clock
from kivy.cache import Cache
Cache.register("anime")
"""
gotta learn how this works :)
"""
# class _Worker(Thread):
# def __init__(self, pool, tasks):
# Thread.__init__(self)
# self.tasks = tasks
# self.daemon = True
# self.pool = pool
# self.start()
# def run(self):
# while self.pool.running:
# func, args, kwargs = self.tasks.get()
# try:
# func(*args, **kwargs)
# except Exception as e:
# print(e)
# self.tasks.task_done()
# class _ThreadPool(object):
# '''Pool of threads consuming tasks from a queue
# '''
# def __init__(self, num_threads):
# super(_ThreadPool, self).__init__()
# self.running = True
# self.tasks = queue.Queue()
# for _ in range(num_threads):
# _Worker(self, self.tasks)
# def add_task(self, func, *args, **kargs):
# '''Add a task to the queue
# '''
# self.tasks.put((func, args, kargs))
# def stop(self):
# self.running = False
# self.tasks.join()
user_data = JsonStore("user_data.json")
my_list = user_data.get("my_list")["list"] # returns a list of anime ids
yt_stream_links = user_data.get("yt_stream_links")["links"]
if yt_stream_links:
for link in yt_stream_links:
Cache.append("anime",link[0],tuple(link[1]))
# for youtube video links gotten from from pytube which is blocking
class MediaCardDataLoader(object):
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("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
# sleep(0.5)
data = preview_image,video_stream_url
yt_stream_links.append((yt_watch_url,data))
user_data.put("yt_stream_links",links=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)
print("Update: ",data)
except Exception as e:
data = None
print(f"Not accesible:{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:
# client.set_preview_image(data[0])
trailer_url = data[1]
print(trailer_url,"-----------------")
if trailer_url:
client.set_trailer_url(trailer_url)
print(client.title,self._running)
Cache.append("anime",yt_watch_link,data)
self._client.remove((c_yt_watch_link, client))
self._trigger_update()
def media_card(self,anime_item,load_callback=None, post_callback=None,
**kwargs):
media_card = MediaCard()
media_card.anime_id = anime_item["id"]
if anime_item["title"]["english"]:
media_card.title = anime_item["title"]["english"]
else:
media_card.title = anime_item["title"]["romaji"]
# if anime_item.get("cover_image"):
media_card.cover_image_url = anime_item["coverImage"]["medium"]
media_card.popularity = str(anime_item["popularity"])
media_card.favourites = str(anime_item["favourites"])
media_card.episodes = str(anime_item["episodes"])
if anime_item.get("description"):
media_card.description = anime_item["description"]
media_card.first_aired_on = f'{anime_item["startDate"]["day"]}-{anime_item["startDate"]["month"]}-{anime_item["startDate"]["year"]}'
media_card.studios = ", ".join([studio["name"] for studio in anime_item["studios"]["nodes"]])
if anime_item.get("tags"):
media_card.tags = ", ".join([tag["name"] for tag in anime_item["tags"]])
media_card.media_status = anime_item["status"]
if anime_item.get("genres"):
media_card.genres = ",".join(anime_item["genres"])
# media_card.characters =
if anime_item["id"] in my_list:
media_card.is_in_my_list = True
if anime_item["averageScore"]:
stars = int(anime_item["averageScore"]/100*6)
if stars:
for i in range(stars):
media_card.stars[i] = 1
if anime_item["trailer"]:
yt_watch_link = "https://youtube.com/watch?v="+anime_item["trailer"]["id"]
data = Cache.get("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('anime',yt_watch_link, (False,False))
self._start_wanted = True
self._trigger_update()
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()
def run(self, *largs):
while self._running:
try:
parameters = self._q_load.pop()
except:
return
self.pool.add_task(self._load, parameters)
MediaCardLoader = LoaderThreadPool()
# Logger.info('Loader: using a thread pool of {} workers'.format(
# Loader.num_workers))

17
app/Utility/observer.py Normal file
View File

@@ -0,0 +1,17 @@
# 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.
# I would like to point out that using Kivy, one could use the on-signaling
# model. In this case, when the state changes, the model will send a signal
# that can be received by all attached observers. This approach seems less
# universal - you may want to use a different library in the future.
class Observer:
"""Abstract superclass for all observers."""
def model_is_changed(self):
"""
The method that will be called on the observer when the model changes.
"""

View File

@@ -0,0 +1,22 @@
from kivymd.uix.snackbar import MDSnackbar,MDSnackbarText,MDSnackbarSupportingText
from kivy.clock import Clock
def show_notification(title,details):
def _show(dt):
MDSnackbar(
MDSnackbarText(
text=title,
),
MDSnackbarSupportingText(
text=details,
shorten=False,
max_lines=0,
adaptive_height=True
),
duration=5,
y="10dp",
pos_hint={"bottom": 1,"right":.99},
padding=[0, 0, "8dp", "8dp"],
size_hint_x=.4
).open()
Clock.schedule_once(_show,1)

View File

View File

@@ -0,0 +1,96 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
# custom sets for existing
<MDBoxLayout>
size_hint: 1,1
# custom components
<CommonNavigationRailItem@MDNavigationRailItem>
icon:""
text:""
<CommonNavigationRailItem>
MDNavigationRailItemIcon:
icon:root.icon
MDNavigationRailItemLabel:
text: root.text
<MainScreenView>
md_bg_color: self.theme_cls.backgroundColor
main_container:main_container
MDBoxLayout:
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home-outline"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
on_release:
# print("r")
root.manager_screens.current = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "Bookmark"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
id:p
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
# do_scroll_y:True
# do_scroll_x:False
size_hint:1,1
# height:main_container.minimum_height
MDBoxLayout:
id:main_container
padding:"50dp","5dp","50dp","150dp"
spacing:"10dp"
orientation: 'vertical'
size_hint_y:None
height:max(self.minimum_height,p.height,1800)
adaptive_height:True
# MDBoxLayout:
# size_hint_y:None
# height:self.minimum_height
# MDLabel:
# text: "By BeneX"

View File

@@ -0,0 +1,16 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView
class MainScreenView(BaseScreenView):
main_container = ObjectProperty()
def write_data(self):
self.controller.write_data()
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.
"""

View File

@@ -0,0 +1,76 @@
<MyListScreenView>
md_bg_color: self.theme_cls.backgroundColor
my_list_container:my_list_container
MDBoxLayout:
size_hint:1,1
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home"
on_release:
root.manager_screens.current = "main screen"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
# on_release:
# root.manager_screens.current_screen = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "Bookmark"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
# ScreenManager:
# MDScreen:
# name:"main"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
id:p
size_hint:1,1
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
required:True
on_text_validate:
root.handle_search_for_anime(args[0])
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
size_hint:1,1
MDGridLayout:
spacing: '10dp'
padding: "75dp","50dp","10dp","100dp"
id:my_list_container
cols:5
size_hint_y:None
height:self.minimum_height

View File

@@ -0,0 +1,28 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty
from View.base_screen import BaseScreenView
class MyListScreenView(BaseScreenView):
my_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 handle_search_for_anime(self,search_widget):
search_term = search_widget.text
if search_term and not(self.is_searching):
self.search_term = search_term
self.search_results_container.clear_widgets()
if self.filters:
self.controller.requested_search_for_anime(search_term,**self.filters)
else:
self.controller.requested_search_for_anime(search_term)
def update_layout(self,widget):
self.search_results_container.add_widget(widget)
def add_pagination(self,pagination_info):
pass

View File

View File

@@ -0,0 +1,80 @@
<SearchScreenView>
md_bg_color: self.theme_cls.backgroundColor
search_results_container:search_results_container
MDBoxLayout:
size_hint:1,1
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home"
on_release:
root.manager_screens.current = "main screen"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
# on_release:
# root.manager_screens.current_screen = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "My Anime List"
on_release:
root.manager_screens.current = "my list screen"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
# ScreenManager:
# MDScreen:
# name:"main"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
id:p
size_hint:1,1
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
required:True
on_text_validate:
root.handle_search_for_anime(args[0])
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
size_hint:1,1
MDGridLayout:
id:search_results_container
spacing: '10dp'
padding: "75dp","75dp","10dp","200dp"
cols:5
size_hint_y:None
height:self.minimum_height

View File

@@ -0,0 +1,31 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty
from View.base_screen import BaseScreenView
class SearchScreenView(BaseScreenView):
search_results_container = ObjectProperty()
search_term = StringProperty()
filters = DictProperty()
is_searching = False
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 handle_search_for_anime(self,search_widget):
search_term = search_widget.text
if search_term and not(self.is_searching):
self.search_term = search_term
self.search_results_container.clear_widgets()
if self.filters:
self.controller.requested_search_for_anime(search_term,**self.filters)
else:
self.controller.requested_search_for_anime(search_term)
def update_layout(self,widget):
self.search_results_container.add_widget(widget)
def add_pagination(self,pagination_info):
pass

3
app/View/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .MainScreen.main_screen import MainScreenView
from .SearchScreen.search_screen import SearchScreenView
from .MylistScreen.my_list_screen import MyListScreenView

45
app/View/base_screen.py Normal file
View File

@@ -0,0 +1,45 @@
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from Utility.observer import Observer
class BaseScreenView(MDScreen, Observer):
"""
A base class that implements a visual representation of the model data.
The view class must be inherited from this class.
"""
controller = ObjectProperty()
"""
Controller object - :class:`~Controller.controller_screen.ClassScreenControler`.
:attr:`controller` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
model = ObjectProperty()
"""
Model object - :class:`~Model.model_screen.ClassScreenModel`.
:attr:`model` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
manager_screens = ObjectProperty()
"""
Screen manager object - :class:`~kivymd.uix.screenmanager.MDScreenManager`.
:attr:`manager_screens` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kw):
super().__init__(**kw)
# Often you need to get access to the application object from the view
# class. You can do this using this attribute.
self.app = MDApp.get_running_app()
# Adding a view class as observer.
self.model.add_observer(self)

View File

@@ -0,0 +1 @@
from .media_card import MediaCard,MediaCardsContainer

View File

@@ -0,0 +1 @@
from .media_card import MediaCard,MediaCardsContainer

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,249 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
#:set yellow [.9,.9,0,.9]
# overides and customization
<MDLabel>:
adaptive_height:True
max_lines:1
valign:"top"
shorten_from:"right"
shorten:True
<MDLabelShortened@MDLabel>
bold:True
<MDIcon>
valign:"center"
<MDBoxLayout>:
# adaptive_height:True
size_hint_y:None
height:self.minimum_height
# custom components
<Tooltip>
MDTooltipPlain:
text:root.tooltip_text
# <TooltipMDIconButton>
<MediaPopup>
size_hint: None, None
height: dp(500)
width: dp(400)
radius:[5,5,5,5]
md_bg_color:self.theme_cls.backgroundColor
anchor_y: 'top'
player:player
MDBoxLayout:
orientation: 'vertical'
adaptive_height:False
anchor_y: 'top'
MDRelativeLayout:
size_hint_y: None
height: dp(250)
line_color:root.caller.has_trailer_color
line_width:2
MediaPopupVideoPlayer:
id:player
source:root.caller.trailer_url
preview:root.caller.preview_image
state:"play" if root.caller.trailer_url else "stop"
fit_mode:"fill"
size_hint_y: None
height: dp(250)
MDBoxLayout:
padding: "10dp","5dp"
spacing:"5dp"
pos_hint: {'left': 1,'top': 1}
adaptive_height:True
MDIcon:
icon: "star"
color:yellow
disabled: not(root.caller.stars[0])
MDIcon:
color:yellow
disabled: not(root.caller.stars[1])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[2])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[3])
icon: "star"
MDIcon:
color:yellow
icon: "star"
disabled: not(root.caller.stars[4])
MDIcon:
color: yellow
icon: "star"
disabled: not(root.caller.stars[5])
MDLabel:
text: f"{root.caller.episodes} Episodes"
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': 0.5}
adaptive_height:True
color: 0,0,0,.7
MDBoxLayout:
padding:"5dp"
pos_hint: {'bottom': 1}
adaptive_height:True
MDLabel:
text:root.caller.media_status
opacity:.8
halign:"left"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
adaptive_height:True
MDLabel:
text:root.caller.first_aired_on
opacity:.8
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
adaptive_height:True
MDBoxLayout:
orientation: 'vertical'
padding:"10dp"
spacing:"10dp"
adaptive_height:False
MDBoxLayout:
adaptive_height:True
MDBoxLayout:
adaptive_height:True
pos_hint: {'center_y': 0.5}
TooltipMDIconButton:
tooltip_text:root.caller.title
icon: "play-circle"
on_press: root.caller.is_play
MDIconButton:
icon: "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
on_release:
root.caller.is_in_my_list = not(root.caller.is_in_my_list)
self.icon = "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
MDIconButton:
icon: "bell-circle" if not(root.caller.is_in_my_notify) else "bell-check"
MDBoxLayout:
orientation: 'vertical'
pos_hint: {'center_y': 0.5}
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Genres: "+"[/color]"+root.caller.genres
markup:True
MDBoxLayout:
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Popularity: "+"[/color]"+root.caller.popularity
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Favourites: "+"[/color]"+root.caller.favourites
MDScrollView:
size_hint:1,1
do_scroll_y:True
MDLabel:
font_style:"Body"
role:"small"
text:root.caller.description
shorten:False
adaptive_height:True
# shorten_from:"right"
max_lines:0
MDBoxLayout:
orientation: 'vertical'
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Author: "+"[/color]"+root.caller.author
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Studios: "+"[/color]"+root.caller.studios
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Characters: "+"[/color]"+root.caller.characters
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Tags: "+"[/color]"+root.caller.tags
<MediaCard>
spacing:"5dp"
image:"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx163270-oxwgbe43Cpog.jpg"
on_release:
self.open()
size_hint_x: None
width:dp(100)
# height:self.minimum_height
FitImage:
source:root.cover_image_url
fit_mode:"fill"
size_hint: None, None
width: dp(100)
height: dp(150)
MDDivider:
color:root.has_trailer_color
MDLabel:
font_style:"Label"
role:"medium"
text:root.title
max_lines:2
halign:"center"
color:self.theme_cls.secondaryColor
<MediaCardsContainer>
# adaptive_height:True
# size_hint_y:None
size_hint_x:1
# height:dp(300)
size_hint_y:None
height:max(self.minimum_height,dp(350),container.minimum_height)
# adaptive_height:True
# width:self.minimum_width
container:container
orientation: 'vertical'
padding:"10dp"
spacing:"5dp"
MDLabel:
text:root.list_name
MDScrollView:
# do_scroll_x:True
# do_scroll_y:False
size_hint:1,None
height:container.minimum_height
MDBoxLayout:
id:container
padding:"0dp","10dp","100dp","10dp"
size_hint:None,None
height:self.minimum_height
width:self.minimum_width
spacing:"10dp"

View File

@@ -0,0 +1,185 @@
import os
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tooltip import MDTooltip
from kivymd.uix.behaviors import BackgroundColorBehavior,StencilBehavior,CommonElevationBehavior,HoverBehavior
from kivymd.uix.button import MDIconButton
from kivymd.theming import ThemableBehavior
from kivy.uix.modalview import ModalView
from kivy.properties import ObjectProperty,StringProperty,BooleanProperty,ListProperty,NumericProperty
from kivy.uix.video import Video
class Tooltip(MDTooltip):
pass
class TooltipMDIconButton(Tooltip,MDIconButton):
tooltip_text = StringProperty()
class MediaPopupVideoPlayer(Video):
# self.prev
pass
class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
caller = ObjectProperty()
player = ObjectProperty()
def __init__(self, caller,*args,**kwarg):
self.caller = caller
super(MediaPopup,self).__init__(*args,**kwarg)
def on_leave(self,*args):
def _leave(dt):
if not self.hovering:
self.dismiss()
Clock.schedule_once(_leave,2)
class MediaCard(ButtonBehavior,HoverBehavior,MDBoxLayout):
title = StringProperty()
is_play = ObjectProperty()
trailer_url = StringProperty()
episodes = StringProperty()
favourites = StringProperty()
popularity = StringProperty()
media_status = StringProperty("Releasing")
is_in_my_list = BooleanProperty(False)
is_in_my_notify = BooleanProperty(False)
genres = StringProperty()
first_aired_on = StringProperty()
description = StringProperty()
author = StringProperty()
studios = StringProperty()
characters = StringProperty()
tags = StringProperty()
stars = ListProperty([0,0,0,0,0,0])
cover_image_url = StringProperty()
preview_image = StringProperty()
# screen_name = StringProperty()
screen = ObjectProperty()
anime_id = NumericProperty()
has_trailer_color = ListProperty([1,1,1,0])
def __init__(self,trailer_url=None,**kwargs):
super().__init__(**kwargs)
self.orientation = "vertical"
if trailer_url:
self.trailer_url = trailer_url
self.adaptive_size = True
# self.app = MDApp.get_running_app()
# def on_screen_name(self,instance,value):
# if self.app:
# self.screen = self.app.manager_screens.get_screen(value)
def on_enter(self):
def _open_popup(dt):
if self.hovering:
window = self.get_parent_window()
for widget in window.children: # type: ignore
if isinstance(widget,MediaPopup):
return
self.open()
Clock.schedule_once(_open_popup,5)
def on_popup_open(self,popup:MediaPopup):
popup.center = self.center
def on_dismiss(self,popup:MediaPopup):
popup.player.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,*_):
popup = MediaPopup(self)
popup.title = self.title
popup.bind(on_dismiss=self.on_dismiss,on_open=self.on_popup_open)
popup.open(self)
# ---------------respond to user actions and call appropriate model-------------------------
def on_is_in_my_list(self,instance,value):
if self.screen:
self.screen.controller.update_my_list(self.anime_id,value)
def on_trailer_url(self,*args):
pass
class MediaCardsContainer(MDBoxLayout):
container = ObjectProperty()
list_name = StringProperty()
# if __name__ == "__main__":
# from kivymd.app import MDApp
# from kivy.lang import Builder
# import json
# import os
# import tracemalloc
# tracemalloc.start()
# data = {}
# with open(os.path.join(os.curdir,"View","components","media_card","data.json"),"r") as file:
# data = json.loads(file.read())
# cache = {}
# def fetch_data(key):
# yt = YouTube(key)
# preview_image = yt.thumbnail_url
# video_stream_url = yt.streams.filter(progressive=True,file_extension="mp4")[-1].url
# return preview_image,video_stream_url
# def cached_fetch_data(key):
# if key not in cache:
# cache[key] = fetch_data(key)
# return cache[key]
# class MediaCardApp(MDApp):
# def build(self):
# self.theme_cls.primary_palette = "Magenta"
# self.theme_cls.theme_style = "Dark"
# ui = Builder.load_file("./media_card.kv")
# for item in data["data"]["Page"]["media"]:
# media_card = MediaCard()
# if item["title"]["english"]:
# media_card.title = item["title"]["english"]
# else:
# media_card.title = item["title"]["romaji"]
# media_card.cover_image_url = item["coverImage"]["medium"]
# media_card.popularity = str(item["popularity"])
# media_card.favourites = str(item["favourites"])
# media_card.episodes = str(item["episodes"])
# media_card.description = item["description"]
# media_card.first_aired_on = str(item["startDate"])
# media_card.studios = str(item["studios"]["nodes"])
# media_card.tags = str(item["tags"])
# media_card.media_status = item["status"]
# if item["trailer"]:
# try:
# url = cached_fetch_data("https://youtube.com/watch?v="+item["trailer"]["id"])[1]
# media_card.trailer_url =url
# except:
# pass
# media_card.genres = ",".join(item["genres"])
# stars = int(item["averageScore"]/100*6)
# if stars:
# for i in range(stars):
# media_card.stars[i] = 1
# ui.ids.cards.add_widget(media_card) # type: ignore
# return ui
# MediaCardApp().run()
# snapshot = tracemalloc.take_snapshot()
# print("-----------------------------------------------")
# for stat in snapshot.statistics("lineno")[:10]:
# print(stat)

21
app/View/screens.py Normal file
View File

@@ -0,0 +1,21 @@
# The screens dictionary contains the objects of the models and controllers
# of the screens of the application.
from Controller import SearchScreenController,MainScreenController,MyListScreenController
from Model import MainScreenModel,SearchScreenModel,MyListScreenModel
screens = {
"main screen": {
"model": MainScreenModel,
"controller": MainScreenController,
},
"search screen": {
"model": SearchScreenModel,
"controller": SearchScreenController,
},
"my list screen": {
"model": MyListScreenModel,
"controller": MyListScreenController,
},
}

0
app/libs/__init__.py Normal file
View File

View File

@@ -0,0 +1 @@
from .anilist import AniList

134
app/libs/anilist/anilist.py Normal file
View File

@@ -0,0 +1,134 @@
from .queries_graphql import (
most_favourite_query,
most_recently_updated_query,
most_popular_query,
trending_query,
most_scored_query,
recommended_query,
search_query,
anime_characters_query,
anime_relations_query,
airing_schedule_query,
upcoming_anime_query
)
import requests
# from kivy.network.urlrequest import UrlRequestRequests
class AniList:
@classmethod
def get_data(cls,query:str,variables:dict = {})->tuple[bool,dict]:
url = "https://graphql.anilist.co"
# req=UrlRequestRequests(url, cls.got_data,)
try:
response = requests.post(url,json={"query":query,"variables":variables},timeout=5)
return (True,response.json())
except requests.exceptions.Timeout:
return (False,{"Error":"Timeout Exceeded for connection there might be a problem with your internet or anilist is down."})
except requests.exceptions.ConnectionError:
return (False,{"Error":"There might be a problem with your internet or anilist is down."})
except Exception as e:
return (False,{"Error":f"{e}"})
@classmethod
def got_data(cls):
pass
@classmethod
def search(cls,
query:str|None=None,
sort:list[str]|None=None,
genre_in:list[str]|None=None,
genre_not_in:list[str]|None=None,
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_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
)->tuple[bool,dict]:
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)
return search_results
@classmethod
def get_trending(cls)->tuple[bool,dict]:
trending = cls.get_data(trending_query)
return trending
@classmethod
def get_most_favourite(cls)->tuple[bool,dict]:
most_favourite = cls.get_data(most_favourite_query)
return most_favourite
@classmethod
def get_most_scored(cls)->tuple[bool,dict]:
most_scored = cls.get_data(most_scored_query)
return most_scored
@classmethod
def get_most_recently_updated(cls)->tuple[bool,dict]:
most_recently_updated = cls.get_data(most_recently_updated_query)
return most_recently_updated
@classmethod
def get_most_popular(cls)->tuple[bool,dict]:
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)->tuple[bool,dict]:
recommended_anime = cls.get_data(recommended_query)
return recommended_anime
@classmethod
def get_charcters_of(cls,id:int)->tuple[bool,dict]:
variables = {"id":id}
characters = cls.get_data(anime_characters_query,variables)
return characters
@classmethod
def get_related_anime_for(cls,id:int)->tuple[bool,dict]:
variables = {"id":id}
related_anime = cls.get_data(anime_relations_query,variables)
return related_anime
@classmethod
def get_airing_schedule_for(cls,id:int)->tuple[bool,dict]:
variables = {"id":id}
airing_schedule = cls.get_data(airing_schedule_query,variables)
return airing_schedule
@classmethod
def get_upcoming_anime(cls,page:int)->tuple[bool,dict]:
variables = {"page":page}
upcoming_anime = cls.get_data(upcoming_anime_query,variables)
return upcoming_anime
if __name__ == "__main__":
import json
# data = AniList.get_most_popular()
# data = AniList.get_most_favourite()
# data = AniList.get_most_recently_updated()
# data = AniList.get_trending()
# data = AniList.get_most_scored()
# term = input("enter term: ")
data = AniList.search(query="Ninja")
# data = AniList.get_recommended_anime_for(21)
# data = AniList.get_related_anime_for(21)
# data = AniList.get_airing_schedule_for(21)
# data = AniList.get_upcoming_anime(1)
print(json.dumps(data,indent=4))
pass

View File

@@ -0,0 +1,576 @@
optional_variables = "\
$page:Int,\
$sort:[MediaSort],\
$genre_in:[String],\
$genre_not_in:[String],\
$tag_in:[String],\
$tag_not_in:[String],\
$status_in:[MediaStatus],\
$status_not_in:[MediaStatus],\
$popularity_greater:Int,\
$popularity_lesser:Int,\
$averageScore_greater:Int,\
$averageScore_lesser:Int,\
$startDate_greater:FuzzyDateInt,\
$startDate_lesser:FuzzyDateInt,\
$endDate_greater:FuzzyDateInt,\
$endDate_lesser:FuzzyDateInt\
"
# FuzzyDateInt = (yyyymmdd)
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
search_query = """
query($query:String,%s){
Page(perPage:15,page:$page){
pageInfo{
total
currentPage
}
media(
search:$query,
genre_in:$genre_in,
genre_not_in:$genre_not_in,
tag_in:$tag_in,
tag_not_in:$tag_not_in,
status_in:$status_in,
status_not_in:$status_not_in,
popularity_greater:$popularity_greater,
popularity_lesser:$popularity_lesser,
averageScore_greater:$averageScore_greater,
averageScore_lesser:$averageScore_lesser,
startDate_greater:$startDate_greater,
startDate_lesser:$startDate_lesser,
endDate_greater:$endDate_greater,
endDate_lesser:$endDate_lesser,
sort:$sort,
type:ANIME
)
{
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
episodes
genres
studios{
nodes{
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
""" % optional_variables
trending_query = """
query{
Page(perPage:15){
media(sort:TRENDING_DESC,type:ANIME){
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
genres
episodes
description
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
# mosts
most_favourite_query = """
query{
Page(perPage:15){
media(sort:FAVOURITES_DESC,type:ANIME){
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
episodes
description
genres
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_scored_query = """
query{
Page(perPage:15){
media(sort:SCORE_DESC,type:ANIME){
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
episodes
favourites
averageScore
description
genres
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_popular_query = """
query{
Page(perPage:15){
media(sort:POPULARITY_DESC,type:ANIME){
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
description
episodes
genres
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
most_recently_updated_query = """
query{
Page(perPage:15){
media(sort:UPDATED_AT_DESC,type:ANIME){
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
description
genres
episodes
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
recommended_query = """
query {
Page(perPage:15) {
media( type: ANIME) {
recommendations(sort:RATING_DESC){
nodes{
media{
id
title{
english
romaji
native
}
coverImage{
medium
}
description
episodes
trailer{
site
id
}
genres
averageScore
popularity
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}
}
}
"""
anime_characters_query = """
query($id:Int){
Page {
media(id:$id, type: ANIME) {
characters {
nodes {
name {
first
middle
last
full
native
}
image {
medium
}
description
gender
dateOfBirth {
year
month
day
}
age
bloodType
favourites
}
}
}
}
}
"""
anime_relations_query = """
query ($id: Int) {
Page(perPage: 20) {
media(id: $id, sort: POPULARITY_DESC, type: ANIME) {
relations {
nodes {
id
title {
english
romaji
native
}
coverImage {
medium
}
description
episodes
trailer {
site
id
}
genres
averageScore
popularity
favourites
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
}
}
"""
airing_schedule_query = """
query ($id: Int) {
Page {
media(id: $id, sort: POPULARITY_DESC, type: ANIME) {
airingSchedule(notYetAired:true){
nodes{
airingAt
timeUntilAiring
episode
}
}
}
}
}
"""
upcoming_anime_query = """
query ($page: Int) {
Page(page: $page) {
pageInfo {
total
perPage
currentPage
hasNextPage
}
media(type: ANIME, status: NOT_YET_RELEASED,sort:POPULARITY_DESC) {
id
title {
romaji
english
}
coverImage {
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
genres
episodes
description
studios {
nodes {
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
# print(search_query)

View File

@@ -0,0 +1,71 @@
query($query:String){
Page(perPage:15,page:$page){
pageInfo{
total
currentPage
}
media(
search:$query,
genre_in:$genre_in,
genre_not_in:$genre_not_in,
tag_in:$tag_in,
tag_not_in:$tag_not_in,
status_in:$status_in,
status_not_in:$status_not_in,
popularity_greater:$popularity_greater,
popularity_lesser:$popularity_lesser,
averageScore_greater:$averageScore_greater,
averageScore_lesser:$averageScore_lesser,
startDate_greater:$startDate_greater,
startDate_lesser:$startDate_lesser,
endDate_greater:$endDate_greater,
endDate_lesser:$endDate_lesser,
sort:$sort,
type:ANIME
)
{
id
title{
romaji
english
}
coverImage{
medium
}
trailer {
site
id
}
popularity
favourites
averageScore
episodes
genres
studios{
nodes{
name
favourites
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}

View File

@@ -0,0 +1 @@
3.12

View File

@@ -0,0 +1 @@
from .animdl_api import AnimdlApi

View File

@@ -0,0 +1,82 @@
from subprocess import run, PIPE
from difflib import SequenceMatcher
import time
import json
import re
class AnimdlApi:
@classmethod
def run_command(cls,cmds:list):
return run(["C:\\Users\\bxavi\\.pyenv\\pyenv-win\\versions\\3.10.11\\python.exe","-m", "animdl", *cmds],capture_output=True,stdin=PIPE,text=True)
@classmethod
def get_anime_match(cls,anime_item,title):
return SequenceMatcher(None,title,anime_item[0]).ratio()
@classmethod
def get_anime_url_by_title(cls,title:str):
result = cls.run_command(["search",title])
possible_animes = cls.output_parser(result)
if possible_animes:
anime_url = max(possible_animes.items(),key=lambda anime_item:cls.get_anime_match(anime_item,title))
return anime_url # {"title","anime url"}
return None
@classmethod
def get_stream_urls_by_anime_url(cls,anime_url:str):
if anime_url:
try:
result = cls.run_command(["grab",anime_url])
return [json.loads(episode.strip()) for episode in result.stdout.strip().split("\n")]
except:
return None
return None
@classmethod
def get_stream_urls_by_anime_title(cls,title:str):
anime = cls.get_anime_url_by_title(title)
if anime:
return anime[0],cls.get_stream_urls_by_anime_url(anime[1])
return None
@classmethod
def contains_only_spaces(cls,input_string):
return all(char.isspace() for char in input_string)
@classmethod
def output_parser(cls,result_of_cmd:str):
data = result_of_cmd.stderr.split("\n")[3:]
parsed_data = {}
pass_next = False
for i,data_item in enumerate(data[:]):
if pass_next:
pass_next = False
continue
if not data_item or cls.contains_only_spaces(data_item):
continue
item = data_item.split(" / ")
numbering = r"^\d*\.\s*"
try:
if item[1] == "" or cls.contains_only_spaces(item[1]):
pass_next = True
parsed_data.update({f"{re.sub(numbering,'',item[0])}":f"{data[i+1]}"})
else:
parsed_data.update({f"{re.sub(numbering,'',item[0])}":f"{item[1]}"})
except:
pass
return parsed_data
if __name__ == "__main__":
# for anime_title,url in AnimdlApi.get_anime_url_by_title("jujutsu").items():
# t = AnimdlApi.get_stream_urls_by_anime_url("https://allanime.to/anime/LYKSutL2PaAjYyXWz")
start = time.time()
t = AnimdlApi.get_stream_urls_by_anime_title("KONOSUBA -God's Blessing on This Wonderful World! 3")
delta = time.time() - start
print(t)
print(f"Took: {delta} secs")

49
app/main.py Normal file
View File

@@ -0,0 +1,49 @@
import os
os.environ["KIVY_VIDEO"] = "ffpyplayer"
from kivymd.icon_definitions import md_icons
import json
import plyer
from kivymd.app import MDApp
from kivy.uix.screenmanager import ScreenManager,FadeTransition
from kivy.clock import Clock
from kivy.storage.jsonstore import JsonStore
user_data = JsonStore("user_data.json")
if not(user_data.exists("my_list")):
user_data.put("my_list",list=[])
if not(user_data.exists("yt_stream_links")):
user_data.put("yt_stream_links",links=[])
from View.screens import screens
# plyer.
class AninformaApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.load_all_kv_files(self.directory)
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
self.manager_screens = ScreenManager()
self.manager_screens.transition = FadeTransition()
def build(self) -> ScreenManager:
self.generate_application_screens()
return self.manager_screens
def on_start(self,*args):
super().on_start(*args)
def generate_application_screens(self) -> None:
for i, name_screen in enumerate(screens.keys()):
model = screens[name_screen]["model"]()
controller = screens[name_screen]["controller"](model)
view = controller.get_view()
view.manager_screens = self.manager_screens
view.name = name_screen
self.manager_screens.add_widget(view)
if __name__ == "__main__":
AninformaApp().run()

44
app/main.spec Normal file
View File

@@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='main',
)

23
app/readme.txt Normal file
View File

@@ -0,0 +1,23 @@
All this instructions should be done from the folder you chose to install
aniXstream but incase you have never installed python should work any where
1. First install pyenv with the following command:
Invoke-WebRequest -UseBasicParsing -Uri
"https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1"
-OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"
2. run the following command:
pyenv --version to check whether installation was a success
3. run pyenv install 3.10 and confirm success by running pyenv -l and check
for 3.10
4. run pyenv local 3.10 (if in anixstream directory) or pyenv global 3.10 (if
in another directory to set python version 3.10 as global interpreter)
5. check if success by running python --version and checking if output is 3.10
6. run python -m pip install animdl
7. check if success by running python -m animdl and if no error then you are
ready to use anixstream to stream anime
8. additionally you can use animdl independently by running python -m animdl
and any arguments specified in the animdl documentation eg python -m animdl
stream naruto
-----------------------------
Now enjoy :)
------------------------------

6
app/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
kivy
pytube
ffpyplayer
plyer
https://github.com/kivymd/KivyMD/archive/master.zip

1
app/user_data.json Normal file

File diff suppressed because one or more lines are too long