added custom streaming features plus created the rest of the views with basic template

This commit is contained in:
Benex254
2024-08-05 09:46:53 +03:00
parent e2a4a7dbb1
commit 780c34a5ba
36 changed files with 459 additions and 36 deletions

8
.gitignore vendored
View File

@@ -166,4 +166,10 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
app/anixstream.ini
app/settings.json
app/user_data.json
app/View/SearchScreen/.search_screen.py.un~
app/View/SearchScreen/search_screen.py~
app/user_data.json

View File

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

View File

@@ -0,0 +1,15 @@
from inspect import isgenerator
from View import CrashLogScreenView
from Model import CrashLogScreenModel
from View.components import MediaCardsContainer
from Utility import show_notification
from kivy.clock import Clock
class CrashLogScreenController:
def __init__(self, model:CrashLogScreenModel):
self.model = model
self.view = CrashLogScreenView(controller=self, model=self.model)
# self.update_anime_view()
def get_view(self) -> CrashLogScreenView:
return self.view

View File

@@ -0,0 +1,15 @@
from inspect import isgenerator
from View import DownloadsScreenView
from Model import DownloadsScreenModel
from View.components import MediaCardsContainer
from Utility import show_notification
from kivy.clock import Clock
class DownloadsScreenController:
def __init__(self, model:DownloadsScreenModel):
self.model = model
self.view = DownloadsScreenView(controller=self, model=self.model)
# self.update_anime_view()
def get_view(self) -> DownloadsScreenView:
return self.view

View File

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

View File

@@ -18,7 +18,8 @@ class HomeScreenController:
def __init__(self, model:HomeScreenModel):
self.model = model # Model.main_screen.MainScreenModel
self.view = HomeScreenView(controller=self, model=self.model)
Clock.schedule_once(lambda _:self.populate_home_screen())
if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1":
Clock.schedule_once(lambda _:self.populate_home_screen())
def get_view(self) -> HomeScreenView:
return self.view

View File

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

View File

@@ -0,0 +1,7 @@
from Model.base_model import BaseScreenModel
class CrashLogScreenModel(BaseScreenModel):
"""
Handles the crashlog screen logic
"""

View File

@@ -0,0 +1,7 @@
from Model.base_model import BaseScreenModel
class DownloadsScreenModel(BaseScreenModel):
"""
Handles the download screen logic
"""

7
app/Model/help_screen.py Normal file
View File

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

View File

@@ -0,0 +1 @@
from .components.animdl_stream_dialog import AnimdlStreamDialog

View File

@@ -34,6 +34,7 @@
height: self.minimum_height
AnimeSideBar:
id:side_bar
screen:root
size_hint_y:None
height:max(self.parent.height,self.minimum_height)
@@ -45,6 +46,7 @@
RankingsBar:
id:rankings_bar
Controls:
screen:root
MDBoxLayout:
adaptive_height:True
padding:"20dp"

View File

@@ -6,8 +6,9 @@ from kivymd.uix.label import MDLabel
from kivy.utils import QueryDict,get_hex_from_color
from collections import defaultdict
from . import AnimdlStreamDialog
# TODO:move the rest of the classes to their own files
class RankingsBar(MDBoxLayout):
rankings = DictProperty(
@@ -22,6 +23,7 @@ class RankingsBar(MDBoxLayout):
class AnimeDescription(MDBoxLayout):
description = StringProperty()
class AnimeCharacter(MDBoxLayout):
voice_actors = ObjectProperty({
"name":"",
@@ -36,6 +38,7 @@ class AnimeCharacter(MDBoxLayout):
"description":""
})
class AnimeCharacters(MDBoxLayout):
container = ObjectProperty()
characters = ListProperty()
@@ -62,6 +65,7 @@ class AnimeCharacters(MDBoxLayout):
# anime_character.voice_actor =
self.container.add_widget(anime_character)
class AnimeReview(MDBoxLayout):
review = ObjectProperty({
"username":"",
@@ -69,6 +73,7 @@ class AnimeReview(MDBoxLayout):
"summary":""
})
class AnimeReviews(MDBoxLayout):
reviews = ListProperty()
container = ObjectProperty()
@@ -83,16 +88,22 @@ class AnimeReviews(MDBoxLayout):
}
self.container.add_widget(review_)
class AnimeHeader(MDBoxLayout):
titles = StringProperty()
banner_image = StringProperty()
class SideBarLabel(MDLabel):
pass
class SideBarHeaderLabel(MDLabel):
pass
class AnimeSideBar(MDBoxLayout):
screen = ObjectProperty()
image = StringProperty()
alternative_titles = DictProperty({
"synonyms":"",
@@ -163,6 +174,10 @@ class AnimeSideBar(MDBoxLayout):
site[1])
self.external_links_container.add_widget(label)
class Controls(MDBoxLayout):
screen = ObjectProperty()
class AnimeScreenView(BaseScreenView):
header:AnimeHeader = ObjectProperty()
side_bar:AnimeSideBar = ObjectProperty()
@@ -170,7 +185,7 @@ class AnimeScreenView(BaseScreenView):
anime_description:AnimeDescription = ObjectProperty()
anime_characters:AnimeCharacters = ObjectProperty()
anime_reviews:AnimeReviews = ObjectProperty()
data = DictProperty()
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
@@ -179,6 +194,7 @@ class AnimeScreenView(BaseScreenView):
"""
def update_layout(self,data):
self.data = data
# uitlity functions
format_date = lambda date_: f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else ""
format_list_with_comma = lambda list_: ", ".join(list_) if list_ else ""
@@ -264,4 +280,10 @@ class AnimeScreenView(BaseScreenView):
self.anime_reviews.reviews = data["reviews"]["nodes"]
# for r in data["recommendation"]["nodes"]:
# r["mediaRecommendation"]
# r["mediaRecommendation"]
def stream_anime_with_custom_cmds_dialog(self):
"""
Called when user wants to stream with custom commands
"""
AnimdlStreamDialog(self.data).open()

View File

@@ -0,0 +1,63 @@
<StreamDialogLabel@MDLabel>:
adaptive_height:True
max_lines:0
shorten:False
markup:True
font_style: "Label"
role: "medium"
bold:True
<StreamDialogHeaderLabel@MDLabel>:
adaptive_height:True
halign:"center"
max_lines:0
shorten:False
bold:True
markup:True
font_style: "Title"
role: "medium"
md_bg_color:self.theme_cls.secondaryContainerColor
padding:"10dp"
<AnimdlStreamDialog>
md_bg_color:self.theme_cls.backgroundColor
radius:8
size_hint:None,None
height:"500dp"
width:"400dp"
MDBoxLayout:
spacing: '10dp'
padding:"10dp"
orientation:"vertical"
StreamDialogHeaderLabel:
text:"Stream on Animdl"
StreamDialogLabel:
text:"Title"
MDTextField:
id:title_field
required:True
StreamDialogLabel:
text:"Range"
MDTextField:
id:range_field
required:True
StreamDialogLabel:
text:"Latest"
MDTextField:
id:latest_field
required:True
StreamDialogLabel:
text:"Quality"
MDTextField:
id:quality_field
required:True
MDBoxLayout:
orientation:"vertical"
MDButton:
pos_hint: {'center_x': 0.5}
on_press:root.stream_anime(app)
MDButtonIcon:
icon:"rss"
MDButtonText:
text:"Stream"

View File

@@ -0,0 +1,34 @@
from kivy.uix.modalview import ModalView
from kivymd.uix.behaviors import StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior
from kivymd.theming import ThemableBehavior
# from main import AniXStreamApp
class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
def __init__(self,data,**kwargs):
super(AnimdlStreamDialog,self).__init__(**kwargs)
self.data = data
if title:=data["title"].get("romaji"):
self.ids.title_field.text = title
elif title:=data["title"].get("english"):
self.ids.title_field.text = title
self.ids.quality_field.text = "best"
def stream_anime(self,app):
cmds = []
title = self.ids.title_field.text
cmds.append(title)
episodes_range = self.ids.range_field.text
if episodes_range:
cmds = [*cmds,"-r",episodes_range]
latest = self.ids.latest_field.text
if latest:
cmds = [*cmds,"-s",latest]
quality = self.ids.quality_field.text
if quality:
cmds = [*cmds,"-q",quality]
# print(title,episodes_range,latest,quality)
print(cmds)
app.watch_on_animdl(custom_options = cmds)

View File

@@ -1,4 +1,4 @@
<Controls@MDBoxLayout>
<Controls>
adaptive_height:True
padding:"10dp"
spacing:"10dp"
@@ -8,10 +8,11 @@
MDButtonText:
text:"Add to MyList"
MDButton:
on_press: print("presed")
on_press:
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog()
MDButtonText:
text:"Watch on Animdl"
MDButton:
on_press: print("presed")
on_press: app.watch_on_allanime(root.screen.data["title"]["romaji"])
MDButtonText:
text:"Watch on AllAnime"

View File

@@ -46,7 +46,8 @@
width:dp(200)
pos_hint: {'center_x': 0.5}
MDButton:
on_press:app.watch_on_animdl(root.alternative_titles)
on_press:
root.screen.stream_anime_with_custom_cmds_dialog()
pos_hint: {'center_x': 0.5}
MDButtonText:

View File

@@ -0,0 +1,18 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<CrashLogScreenView>
md_bg_color: self.theme_cls.backgroundColor
# main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDLabel:
text:"Crash Log"

View File

@@ -0,0 +1,12 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView
class CrashLogScreenView(BaseScreenView):
main_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.
"""

View File

@@ -0,0 +1,18 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<DownloadsScreenView>
md_bg_color: self.theme_cls.backgroundColor
# main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDLabel:
text:"Downloads"

View File

@@ -0,0 +1,16 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView
from kivy.uix.modalview import ModalView
class DownloadAnimePopup(ModalView):
pass
class DownloadsScreenView(BaseScreenView):
main_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.
"""

View File

@@ -0,0 +1,18 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
<HelpScreenView>
md_bg_color: self.theme_cls.backgroundColor
# main_container:main_container
MDBoxLayout:
NavRail:
screen:root
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
SearchBar:
MDLabel:
text:"Help Screen"

View File

@@ -0,0 +1,13 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView
class HelpScreenView(BaseScreenView):
main_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.
"""

View File

@@ -1,4 +1,12 @@
# screens
from .HomeScreen.home_screen import HomeScreenView
from .SearchScreen.search_screen import SearchScreenView
from .MylistScreen.my_list_screen import MyListScreenView
from .AnimeScreen.anime_screen import AnimeScreenView
from .AnimeScreen.anime_screen import AnimeScreenView
from .CrashLogScreen.crashlog_screen import CrashLogScreenView
from .DownloadsScreen.download_screen import DownloadsScreenView
from .HelpScreen.help_screen import HelpScreenView
# others
from .components.animdl_dialog.animdl_dialog import AnimdlDialogPopup
from .DownloadsScreen.download_screen import DownloadAnimePopup

View File

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

View File

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

View File

@@ -30,14 +30,21 @@
on_press:
root.screen.manager_screens.current = "my list screen"
CommonNavigationRailItem:
icon: "library"
text: "Library"
icon: "download-circle"
text: "Downloads"
on_press:
root.screen.manager_screens.current = "anime screen"
root.screen.manager_screens.current = "downloads screen"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
on_press:app.open_settings()
CommonNavigationRailItem:
icon: "help-circle"
text: "Help"
on_press:
root.screen.manager_screens.current = "help screen"
CommonNavigationRailItem:
icon: "bug"
text: "debug"
on_press:
root.screen.manager_screens.current = "crashlog screen"

View File

@@ -1,5 +1,5 @@
from Controller import SearchScreenController,HomeScreenController,MyListScreenController,AnimeScreenController
from Model import HomeScreenModel,SearchScreenModel,MyListScreenModel,AnimeScreenModel
from Controller import (SearchScreenController,HomeScreenController,MyListScreenController,AnimeScreenController,DownloadsScreenController,HelpScreenController,CrashLogScreenController)
from Model import (HomeScreenModel,SearchScreenModel,MyListScreenModel,AnimeScreenModel,DownloadsScreenModel,HelpScreenModel,CrashLogScreenModel)
screens = {
@@ -19,4 +19,16 @@ screens = {
"model": AnimeScreenModel,
"controller": AnimeScreenController,
},
"crashlog screen": {
"model": CrashLogScreenModel,
"controller": CrashLogScreenController,
},
"downloads screen": {
"model": DownloadsScreenModel,
"controller": DownloadsScreenController,
},
"help screen": {
"model": HelpScreenModel,
"controller": HelpScreenController,
},
}

View File

@@ -1 +0,0 @@
3.12

Binary file not shown.

View File

@@ -19,6 +19,21 @@ class AnimdlApi:
else:
return run([py_path,"-m", "animdl", *cmds])
@classmethod
def run_custom_command(cls,*cmds:tuple[str])->Popen:
"""
Runs an AnimDl custom command with the full power of animdl and returns a subprocess(popen) for full control
"""
# TODO: parse the commands
parsed_cmds = list(cmds)
if py_path:=shutil.which("python"):
base_cmds = [py_path,"-m","animdl"]
child_process = Popen([*base_cmds,*parsed_cmds])
return child_process
@classmethod
def stream_anime_by_title(cls,title,episodes_range=None):
anime = cls.get_anime_url_by_title(title)
@@ -183,7 +198,7 @@ class AnimdlApi:
possible_animes = cls.output_parser(result)
if possible_animes:
anime = max(possible_animes.items(),key=lambda anime_item:cls.get_anime_match(anime_item,title))
return anime # {"title","anime url"}
return anime # ("title","anime url")
return None
@classmethod

View File

@@ -13,6 +13,11 @@ import json
from queue import Queue
from threading import Thread
import plyer
# plyer.facades.StoragePath.get_application_dir
# plyer.facades.StoragePath.get_downloads_dir
# plyer.facades.StoragePath.get_videos_dir()
# plyer.facades.StoragePath.
# plyer.facades.StoragePath.get_application_dir
from kivymd.app import MDApp
from kivy.uix.settings import SettingsWithSidebar,Settings
@@ -22,9 +27,11 @@ from kivy.storage.jsonstore import JsonStore
from datetime import date,datetime
from subprocess import Popen
from View.screens import screens
from View import DownloadAnimePopup,AnimdlDialogPopup
import time
from Utility import themes_available
import webbrowser
from Utility import themes_available,show_notification
user_data = JsonStore("user_data.json")
today = date.today()
@@ -43,13 +50,18 @@ elif not( yt_cache.get("yt_stream_links").get(f"{links_cache_name}")):
class AniXStreamApp(MDApp):
queue = Queue()
downloads_queue = Queue()
animdl_streaming_subprocess:Popen|None = None
def worker(self,queue:Queue):
while True:
task = queue.get() # task should be a function
task()
self.queue.task_done()
def downloads_worker(self,queue:Queue):
while True:
download_task = queue.get() # task should be a function
download_task()
self.queue.task_done()
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -64,6 +76,11 @@ class AniXStreamApp(MDApp):
self.worker_thread.daemon = True
self.worker_thread.start()
# initialize downloads worker
self.downloads_worker_thread = Thread(target=self.downloads_worker,args=(self.downloads_queue,))
self.downloads_worker_thread.daemon = True
self.downloads_worker_thread.start()
def build(self) -> ScreenManager:
self.settings_cls = SettingsWithSidebar
self.generate_application_screens()
@@ -92,8 +109,10 @@ class AniXStreamApp(MDApp):
config.setdefaults('Preferences', {
'theme_color': 'Cyan',
"theme_style": "Dark",
"downloads_dir":"."
"downloads_dir": plyer.storagepath.get_videos_dir() if plyer.storagepath.get_videos_dir() else ".",
"is_startup_anime_enable":False
})
print(self.config.get("Preferences","is_startup_anime_enable"))
def build_settings(self,settings:Settings):
settings.add_json_panel("Settings",self.config,"settings.json")
@@ -110,7 +129,14 @@ class AniXStreamApp(MDApp):
config.write()
case "theme_style":
self.theme_cls.theme_style = value
def on_stop(self):
if self.animdl_streaming_subprocess:
self.animdl_streaming_subprocess.terminate()
# custom methods
# TODO: move theme to a personalized class
def search_for_anime(self,search_field,**kwargs):
if self.manager_screens.current != "search screen":
self.manager_screens.current = "search screen"
@@ -120,21 +146,75 @@ class AniXStreamApp(MDApp):
self.manager_screens.current = "anime screen"
self.anime_screen.controller.update_anime_view(id)
def stream_anime_with_animdl(self,title):
self.animdl_streaming_subprocess = AnimdlApi.stream_anime_by_title(title)
self.stop_streaming = False
def watch_on_allanime(self,title_):
"""
Opens the given anime in your default browser on allanimes site
Parameters:
----------
title_: The anime title requested to be opened
"""
if anime:=AnimdlApi.get_anime_url_by_title(title_):
title,link = anime
parsed_link = f"https://allmanga.to/bangumi/{link.split('/')[-1]}"
else:
show_notification("Failure",f"Failed to open {title} in browser on allanime site")
if webbrowser.open(parsed_link):
show_notification("Success",f"Successfully opened {title} in browser allanime site")
else:
show_notification("Failure",f"Failed to open {title} in browser on allanime site")
def open_download_anime_dialog(self):
""""
calls the download dialog
"""
DownloadAnimePopup().open()
def download_anime(self,on_complete,on_progress,default_cmds:dict[str,str]|None=None,custom_cmds:tuple[str]|None=None):
# TODO:Add custom download cmds functionality
output_path = self.config.get("Preferences","downloads_dir")
if default_cmds:
if episodes_range:=default_cmds.get("episodes_range"):
download_task =lambda: AnimdlApi.download_anime_by_title(default_cmds["title"],on_progress,on_complete,output_path,episodes_range,default_cmds["quality"])
self.downloads_queue.put(download_task)
else:
download_task =lambda: AnimdlApi.download_anime_by_title(default_cmds["title"],on_progress,on_complete,output_path,None,default_cmds["quality"])
self.downloads_queue.put(download_task)
def stream_anime_with_custom_input_cmds(self,*cmds):
self.animdl_streaming_subprocess = AnimdlApi.run_custom_command("stream",*cmds)
def stream_anime_by_title_with_animdl(self,title,episodes_range:str|None=None):
self.animdl_streaming_subprocess = AnimdlApi.stream_anime_by_title(title,episodes_range)
# self.stop_streaming = False
def watch_on_animdl(self,title_dict:dict):
def watch_on_animdl(self,title_dict:dict|None=None,episodes_range:str|None=None,custom_options:tuple[str]|None=None):
"""
Enables you to stream an anime using animdl either by parsing a title or custom animdl options
parameters:
-----------
title_dict:dict["japanese","kanji"]
a dictionary containing the titles of the anime
custom_options:tuple[str]
a tuple containing valid animdl stream commands
"""
if self.animdl_streaming_subprocess:
self.animdl_streaming_subprocess.terminate()
if title:=title_dict.get("japanese"):
stream_func = lambda: self.stream_anime_with_animdl(title)
self.queue.put(stream_func)
elif title:=title_dict.get("english"):
stream_func = lambda:self.stream_anime_with_animdl(title)
if title_dict:
if title:=title_dict.get("japanese"):
stream_func = lambda: self.stream_anime_by_title_with_animdl(title,episodes_range)
self.queue.put(stream_func)
elif title:=title_dict.get("english"):
stream_func = lambda:self.stream_anime_by_title_with_animdl(title,episodes_range)
self.queue.put(stream_func)
else:
stream_func = lambda:self.stream_anime_with_custom_input_cmds(*custom_options)
self.queue.put(stream_func)
if __name__ == "__main__":
# try:
AniXStreamApp().run()
# except:
# print(plyer.storagepath.get_videos_dir())

View File

@@ -1 +1 @@
{"my_list": {"list": [1535, 20605, 21519, 21, 5114, 133007, 166710, 116674, 11061, 21745, 9253, 153406, 166613, 133845, 143103, 20996, 162804]}}
{"my_list": {"list": [1535, 20605, 21519, 21, 5114, 133007, 166710, 116674, 11061, 21745, 9253, 153406, 166613, 133845, 143103, 20996, 162804, 125367, 21827]}}