mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-27 13:13:25 -08:00
completed the basic ui and anilist module
This commit is contained in:
1
app/.python-version
Normal file
1
app/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
3
app/Controller/__init__.py
Normal file
3
app/Controller/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main_screen import MainScreenController
|
||||
from .search_screen import SearchScreenController
|
||||
from .my_list_screen import MyListScreenController
|
||||
95
app/Controller/main_screen.py
Normal file
95
app/Controller/main_screen.py
Normal 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)
|
||||
|
||||
21
app/Controller/my_list_screen.py
Normal file
21
app/Controller/my_list_screen.py
Normal 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
|
||||
23
app/Controller/search_screen.py
Normal file
23
app/Controller/search_screen.py
Normal 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
3
app/Model/__init__.py
Normal 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
33
app/Model/base_model.py
Normal 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
73
app/Model/main_screen.py
Normal 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)
|
||||
|
||||
25
app/Model/my_list_screen.py
Normal file
25
app/Model/my_list_screen.py
Normal 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
|
||||
25
app/Model/search_screen.py
Normal file
25
app/Model/search_screen.py
Normal 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
2
app/Utility/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .media_card_loader import MediaCardLoader
|
||||
from .show_notification import show_notification
|
||||
256
app/Utility/media_card_loader.py
Normal file
256
app/Utility/media_card_loader.py
Normal 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
17
app/Utility/observer.py
Normal 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.
|
||||
"""
|
||||
22
app/Utility/show_notification.py
Normal file
22
app/Utility/show_notification.py
Normal 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)
|
||||
0
app/View/MainScreen/__init__.py
Normal file
0
app/View/MainScreen/__init__.py
Normal file
0
app/View/MainScreen/components/__init__.py
Normal file
0
app/View/MainScreen/components/__init__.py
Normal file
96
app/View/MainScreen/main_screen.kv
Normal file
96
app/View/MainScreen/main_screen.kv
Normal 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"
|
||||
|
||||
16
app/View/MainScreen/main_screen.py
Normal file
16
app/View/MainScreen/main_screen.py
Normal 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.
|
||||
"""
|
||||
|
||||
76
app/View/MylistScreen/my_list_screen.kv
Normal file
76
app/View/MylistScreen/my_list_screen.kv
Normal 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
|
||||
|
||||
|
||||
28
app/View/MylistScreen/my_list_screen.py
Normal file
28
app/View/MylistScreen/my_list_screen.py
Normal 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
|
||||
0
app/View/SearchScreen/__init__.py
Normal file
0
app/View/SearchScreen/__init__.py
Normal file
80
app/View/SearchScreen/search_screen.kv
Normal file
80
app/View/SearchScreen/search_screen.kv
Normal 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
|
||||
|
||||
|
||||
31
app/View/SearchScreen/search_screen.py
Normal file
31
app/View/SearchScreen/search_screen.py
Normal 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
3
app/View/__init__.py
Normal 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
45
app/View/base_screen.py
Normal 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)
|
||||
1
app/View/components/__init__.py
Normal file
1
app/View/components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .media_card import MediaCard,MediaCardsContainer
|
||||
1
app/View/components/media_card/__init__.py
Normal file
1
app/View/components/media_card/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .media_card import MediaCard,MediaCardsContainer
|
||||
1355
app/View/components/media_card/data.json
Normal file
1355
app/View/components/media_card/data.json
Normal file
File diff suppressed because it is too large
Load Diff
249
app/View/components/media_card/media_card.kv
Normal file
249
app/View/components/media_card/media_card.kv
Normal 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"
|
||||
185
app/View/components/media_card/media_card.py
Normal file
185
app/View/components/media_card/media_card.py
Normal 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
21
app/View/screens.py
Normal 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
0
app/libs/__init__.py
Normal file
1
app/libs/anilist/__init__.py
Normal file
1
app/libs/anilist/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .anilist import AniList
|
||||
134
app/libs/anilist/anilist.py
Normal file
134
app/libs/anilist/anilist.py
Normal 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
|
||||
576
app/libs/anilist/queries_graphql.py
Normal file
576
app/libs/anilist/queries_graphql.py
Normal 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)
|
||||
71
app/libs/anilist/test.graphql
Normal file
71
app/libs/anilist/test.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1
app/libs/animdl/.python-version
Normal file
1
app/libs/animdl/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
1
app/libs/animdl/__init__.py
Normal file
1
app/libs/animdl/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .animdl_api import AnimdlApi
|
||||
82
app/libs/animdl/animdl_api.py
Normal file
82
app/libs/animdl/animdl_api.py
Normal 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
49
app/main.py
Normal 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
44
app/main.spec
Normal 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
23
app/readme.txt
Normal 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
6
app/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
kivy
|
||||
pytube
|
||||
ffpyplayer
|
||||
plyer
|
||||
https://github.com/kivymd/KivyMD/archive/master.zip
|
||||
|
||||
1
app/user_data.json
Normal file
1
app/user_data.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user