mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 00:00:01 -08:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d62915f2b | ||
|
|
a4e9e5f29e | ||
|
|
d00c958ff2 | ||
|
|
bc2ac69b9a | ||
|
|
01fa96c27a | ||
|
|
6c1bbfe50a | ||
|
|
ecc4e85079 | ||
|
|
1cd743acdf | ||
|
|
23dd969d37 | ||
|
|
d21f6b5ab0 | ||
|
|
640bb12c44 | ||
|
|
453e4c1b74 | ||
|
|
4dc3d1b0bb | ||
|
|
4df57f9410 | ||
|
|
baa94efc24 | ||
|
|
f5d18512f8 | ||
|
|
72037eea07 | ||
|
|
f5c120ebb8 | ||
|
|
5f2b88bd9b | ||
|
|
b346801dba | ||
|
|
1b1a05e2b3 | ||
|
|
8716fb2e1d | ||
|
|
12a38d6d48 | ||
|
|
e6aa508644 | ||
|
|
584a2ee3f1 | ||
|
|
385dd4337d | ||
|
|
1c70a2122d | ||
|
|
46b9b844d4 | ||
|
|
272042ec35 | ||
|
|
56632cf77c | ||
|
|
e8dacf0722 | ||
|
|
b95d49429c | ||
|
|
ca087b2e94 | ||
|
|
3f33ae3738 | ||
|
|
94a282a320 | ||
|
|
0b379ec813 | ||
|
|
6b0a013705 | ||
|
|
6c1f8d09e6 | ||
|
|
6bb2c89a8c | ||
|
|
9f56b74ff0 | ||
|
|
4d03b86498 | ||
|
|
fab86090a3 | ||
|
|
71d258385c | ||
|
|
bc55ed6e81 | ||
|
|
197bfa9f8a | ||
|
|
f84c60e6bc | ||
|
|
d8b94cbbca | ||
|
|
dd4462f42a | ||
|
|
0f9e08b9fa | ||
|
|
01333ab1d1 | ||
|
|
d8bf9e18c4 | ||
|
|
bc909397d5 | ||
|
|
f3d88f9825 | ||
|
|
eb7bef72b3 | ||
|
|
f6ec094bc7 | ||
|
|
3f1bf1781a | ||
|
|
21167fc208 | ||
|
|
c7c6ff92c4 | ||
|
|
78319731c0 | ||
|
|
b619a11db1 | ||
|
|
022420aa4c | ||
|
|
a7e46d9c18 | ||
|
|
5e2826be4e | ||
|
|
5e314e2bca | ||
|
|
3d23854d89 | ||
|
|
80a25d24a3 | ||
|
|
1ad7929c66 | ||
|
|
0670bd735c | ||
|
|
400a600bfe | ||
|
|
b9a3f170ab | ||
|
|
9309ba15b5 | ||
|
|
b2971e0233 | ||
|
|
06f67624d4 | ||
|
|
597c1bc9fd | ||
|
|
6fccd08e96 | ||
|
|
0e9294d7a2 | ||
|
|
c76a354d1b | ||
|
|
215def909e | ||
|
|
edd394ca74 | ||
|
|
af69046025 | ||
|
|
6379c28fed | ||
|
|
23b22dfc70 | ||
|
|
da06b0b6e1 | ||
|
|
68640202c3 | ||
|
|
2595ac5bf7 | ||
|
|
19f2898b73 | ||
|
|
69ec3ebfd7 | ||
|
|
d048bccaa1 | ||
|
|
2c2f2be26d | ||
|
|
7e2c03d54c | ||
|
|
62619421d6 | ||
|
|
84cea644e7 | ||
|
|
85326b9bc6 | ||
|
|
06c602e663 | ||
|
|
54161f13e4 | ||
|
|
d74d93da59 | ||
|
|
0a5fc0fa3c | ||
|
|
52fa6912be | ||
|
|
62bb1f7944 | ||
|
|
6fa88dd959 | ||
|
|
a853c01e52 | ||
|
|
a971b22d72 | ||
|
|
0d64a9bd32 | ||
|
|
16dc63c177 | ||
|
|
b5456635c7 | ||
|
|
d865086a50 | ||
|
|
82272cdf4e | ||
|
|
81aac99da8 | ||
|
|
962bde00a7 | ||
|
|
1d9c911ea1 | ||
|
|
cf3a963173 | ||
|
|
a88e72e4c2 | ||
|
|
269b1447f6 | ||
|
|
e589a92147 | ||
|
|
7fcd5c3475 | ||
|
|
e695577881 | ||
|
|
bcd8637b31 | ||
|
|
8d4f2a8f04 | ||
|
|
6d077fd3e2 | ||
|
|
73ce357789 | ||
|
|
53823f02c1 | ||
|
|
148619029d | ||
|
|
f08062ee71 | ||
|
|
2aa02d6ab9 | ||
|
|
520bfcbb52 | ||
|
|
7d82a356b1 | ||
|
|
be4cacf9dc | ||
|
|
f3b398d344 | ||
|
|
1ffb122cec | ||
|
|
84b8bd9950 | ||
|
|
ab76689f07 | ||
|
|
8c838a82f7 | ||
|
|
9996af900f | ||
|
|
4f0a752033 | ||
|
|
3b8a565843 | ||
|
|
4b5ff6348e | ||
|
|
4a2c981dff | ||
|
|
f93d524f68 | ||
|
|
03a3d32ce4 | ||
|
|
8615960300 | ||
|
|
1442346f07 | ||
|
|
89df10e377 | ||
|
|
7bab3d63e6 | ||
|
|
4bdfe5449e | ||
|
|
d8afdce467 | ||
|
|
14f486a66f | ||
|
|
711686da92 | ||
|
|
37b7702db1 | ||
|
|
18356c41ec | ||
|
|
3bc9a09b1e | ||
|
|
4e705a9d0b | ||
|
|
814646115b | ||
|
|
516333cb13 | ||
|
|
3cd84e2f6f | ||
|
|
a3b8af4a30 | ||
|
|
67c07d350a | ||
|
|
821eb38170 | ||
|
|
b265d66859 | ||
|
|
d11ab7c881 | ||
|
|
62b1e3260a | ||
|
|
912736c66a | ||
|
|
4a989b995e | ||
|
|
e09b3f1fc8 | ||
|
|
ef0083fa5a | ||
|
|
018bccbaab | ||
|
|
2fc4faab05 | ||
|
|
380c2fa42c | ||
|
|
93d94a15ef | ||
|
|
9e40683e86 | ||
|
|
290167fbb5 | ||
|
|
7d029619b5 | ||
|
|
d10e739f07 | ||
|
|
97cb32ce4a | ||
|
|
036be30309 | ||
|
|
30ca30d34e | ||
|
|
7d0b507b2d | ||
|
|
a11a73cf8f | ||
|
|
aec2278749 | ||
|
|
1284ff1c4c | ||
|
|
79418141bb | ||
|
|
03e7d67266 | ||
|
|
48d29bcfc2 | ||
|
|
4cb5a5455c | ||
|
|
96f10faf6b | ||
|
|
b6e6ab13a6 | ||
|
|
8e6dad3732 | ||
|
|
9aeb96a252 | ||
|
|
257faaa3bb | ||
|
|
950e403f07 | ||
|
|
4ca1c70e86 | ||
|
|
76f22795d9 | ||
|
|
cdf507eaf2 | ||
|
|
54bea09efb |
35
.github/workflows/build.yml
vendored
Normal file
35
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: debug_build
|
||||
on: push
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
# see details (matrix, python-version, python-version-file, etc.)
|
||||
# https://github.com/actions/setup-python
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install
|
||||
- name: build app
|
||||
run: poetry build
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fastanime_debug_build
|
||||
path: |
|
||||
dist
|
||||
!dist/*.whl
|
||||
# - name: Run the automated tests (for example)
|
||||
# run: poetry run pytest -v
|
||||
66
.github/workflows/publish.yml
vendored
Normal file
66
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
# NOTE: put your own distribution build steps here.
|
||||
python -m pip install build
|
||||
python -m build
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
pypi-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs:
|
||||
- release-build
|
||||
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
|
||||
# Dedicated environments with protections for publishing are strongly recommended.
|
||||
environment:
|
||||
name: pypi
|
||||
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
||||
# url: https://pypi.org/p/YOURPROJECT
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -2,9 +2,11 @@
|
||||
*.mp4
|
||||
*.mp3
|
||||
*.ass
|
||||
user_data.json
|
||||
yt_cache.json
|
||||
vids/*
|
||||
vids
|
||||
data/
|
||||
.project/
|
||||
fastanime.ini
|
||||
crashdump.txt
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -12,12 +14,12 @@ __pycache__/
|
||||
anixstream.ini
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
bin/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -173,3 +175,4 @@ app/user_data.json
|
||||
app/View/SearchScreen/.search_screen.py.un~
|
||||
app/View/SearchScreen/search_screen.py~
|
||||
app/user_data.json
|
||||
.buildozer
|
||||
|
||||
37
.pre-commit-config.yaml
Normal file
37
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0 # You can replace this with the latest version
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"] # Ensure compatibility with Black
|
||||
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: ["--in-place","--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
name: black
|
||||
language_version: python3.10
|
||||
# ------ TODO: re-add this -----
|
||||
# - repo: https://github.com/PyCQA/bandit
|
||||
# rev: 1.7.9 # Update me!
|
||||
# hooks:
|
||||
# - id: bandit
|
||||
# args: ["-c", "pyproject.toml"]
|
||||
# additional_dependencies: ["bandit[toml]"]
|
||||
@@ -1 +0,0 @@
|
||||
3.10
|
||||
37
LICENSE
37
LICENSE
@@ -1,19 +1,24 @@
|
||||
Copyright (c) 2018 The Python Packaging Authority
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
|
||||
377
README.md
377
README.md
@@ -1,24 +1,381 @@
|
||||
# FastAnime
|
||||
|
||||
# AniXStream
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
This project was created as a gui solution to any and all anime cli's. Thus freeing the cli developer from worrying about making it look good and instead focus on functionality.
|
||||
[fa_demo.webm](https://github.com/user-attachments/assets/bb46642c-176e-42b3-a533-ff55d4dac111)
|
||||
|
||||
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
||||
|
||||
<!--toc:start-->
|
||||
|
||||
- [FastAnime](#fastanime)
|
||||
- [Installation](#installation)
|
||||
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
|
||||
- [Using pipx](#using-pipx)
|
||||
- [Using pip](#using-pip)
|
||||
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
|
||||
- [Building from the source](#building-from-the-source)
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command](#the-anilist-command)
|
||||
- [Running without any subcommand](#running-without-any-subcommand)
|
||||
- [Subcommands](#subcommands)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
<!--toc:end-->
|
||||
|
||||
## Authors
|
||||
|
||||
- [@Benex254](https://github.com/Benex254/aniXstream)
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime and is in no way related to them. The site is in the public domain and can be access by any one with a browser.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The docs are still being worked on and are far from completion.
|
||||
|
||||
## Installation
|
||||
|
||||
installing using the zip file
|
||||
The app can run wherever python can run. So all you need to have is python installed on your device.
|
||||
On android you can use [termux](https://github.com/termux/termux-app).
|
||||
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HRjySFjQ)
|
||||
|
||||
Go to releases and download the zip file.Then unzip it and run the following commands in the same path as the unzipped foder:
|
||||
### Installation using your favourite package manager
|
||||
|
||||
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
|
||||
The app is published approximately after every 14 days, which will include accumulative changes during that period.
|
||||
|
||||
#### Using pipx
|
||||
|
||||
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
|
||||
|
||||
pipx install fastanime
|
||||
```
|
||||
|
||||
|
||||
#### Using pip
|
||||
|
||||
```bash
|
||||
pip install fastanime
|
||||
```
|
||||
|
||||
### Installing the bleeding edge version
|
||||
|
||||
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
|
||||
Then:
|
||||
|
||||
```bash
|
||||
unzip fastanime_debug_build
|
||||
|
||||
# outputs fastanime<version>.tar.gz
|
||||
|
||||
pipx install fastanime<version>.tar.gz
|
||||
|
||||
# --- or ---
|
||||
|
||||
pip install fastanime<version>.tar.gz
|
||||
```
|
||||
|
||||
### Building from the source
|
||||
|
||||
Requirements:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [python 3.10 and above](https://www.python.org/)
|
||||
- [poetry](https://python-poetry.org/docs/#installation)
|
||||
|
||||
To build from the source, follow these steps:
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git`
|
||||
2. Navigate into the folder: `cd FastAnime`
|
||||
3. Then build and Install the app:
|
||||
|
||||
```bash
|
||||
# Normal Installation
|
||||
poetry build
|
||||
cd dist
|
||||
pip install fastanime<version>.whl
|
||||
|
||||
# Editable installation (easiest for updates)
|
||||
# just do a git pull in the Project dir
|
||||
# the latter will require rebuilding the app
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
4. Enjoy! Verify installation with:
|
||||
|
||||
```bash
|
||||
fastanime --version
|
||||
```
|
||||
|
||||
> [!Tip]
|
||||
>
|
||||
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell.
|
||||
> To add completions:
|
||||
>
|
||||
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
|
||||
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
|
||||
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
|
||||
|
||||
### External Dependencies
|
||||
|
||||
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The project currently sees no reason to support any other video
|
||||
> player because we believe nothing beats **MPV** and it provides
|
||||
> everything you could ever need with a small footprint.
|
||||
> But if you have a reason feel free to encourage as to do so.
|
||||
|
||||
**Other dependencies that will just make your experience better:**
|
||||
|
||||
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
|
||||
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
|
||||
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
|
||||
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
|
||||
- [ani-skip](https://github.com/synacktraa/ani-skip) :fire: used for skipping the opening and ending theme songs
|
||||
|
||||
## Usage
|
||||
|
||||
The app offers both a graphical interface (under development) and a robust command-line interface.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The GUI is mostly in hiatus; use the CLI for now.
|
||||
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
|
||||
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
|
||||
|
||||
### The Commandline interface :fire:
|
||||
|
||||
Designed for power users who prefer efficiency over browser-based streaming and still want the experience in their terminal.
|
||||
|
||||
Overview of main commands:
|
||||
|
||||
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
|
||||
- `fastanime download`: Download anime.
|
||||
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
|
||||
- `fastanime downloads`: View downloaded anime and watch with MPV.
|
||||
- `fastanime config`: Quickly edit configuration settings.
|
||||
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
Available options include:
|
||||
|
||||
- `--server;-s <server>` set the default server to auto select
|
||||
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
|
||||
- `--quality;-q <0|1|2|3>` the link to choose from server
|
||||
- `--translation-type;- <dub|sub` what language for anime
|
||||
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
|
||||
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
|
||||
- `-downloads-dir;-d <path>` set the folder to download anime into
|
||||
- `--fzf` use fzf for the ui
|
||||
- `--default` use the default ui
|
||||
- `--preview` show a preview when using fzf
|
||||
- `--no-preview` dont show a preview when using fzf
|
||||
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
|
||||
|
||||
##### Running without any subcommand
|
||||
|
||||
Run `fastanime anilist` to access the main interface.
|
||||
|
||||
##### Subcommands
|
||||
|
||||
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
||||
|
||||
- `fastanime anilist trending`: Top 15 trending anime.
|
||||
- `fastanime anilist recent`: Top 15 recently updated anime.
|
||||
- `fastanime anilist search`: Search for anime (top 50 results).
|
||||
- `fastanime anilist upcoming`: Top 15 upcoming anime.
|
||||
- `fastanime anilist popular`: Top 15 popular anime.
|
||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||
- `fastanime anilist random`: get random anime
|
||||
|
||||
The following are commands you can only run if you are signed in to your AniList account:
|
||||
|
||||
- `fastanime anilist watching`
|
||||
- `fastanime anilist planning`
|
||||
- `fastanime anilist repeating`
|
||||
- `fastanime anilist dropped`
|
||||
- `fastanime anilist paused`
|
||||
- `fastanime anilist completed`
|
||||
|
||||
Plus: `fastanime anilist notifier` :fire:
|
||||
|
||||
```bash
|
||||
# basic form
|
||||
fastanime anilist notifier
|
||||
|
||||
# with logging to stdout
|
||||
fastanime --log anilist notifier
|
||||
|
||||
# with logging to a file. stored in the same place as your config
|
||||
fastanime --log-file anilist notifier
|
||||
```
|
||||
|
||||
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
|
||||
|
||||
The notification will consist of a cover image of the anime in none windows systems.
|
||||
|
||||
You can place the command among your machines startup scripts.
|
||||
|
||||
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
|
||||
|
||||
```fish
|
||||
if ! ps aux | grep -q '[f]astanime .* notifier'
|
||||
echo initializing fastanime anilist notifier
|
||||
fastanime --log-file anilist notifier>/dev/null &
|
||||
end
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To sign in just run `fastanime anilist login` and follow the instructions.
|
||||
> To view your login status `fastanime anilist login --status`
|
||||
|
||||
#### download subcommand
|
||||
|
||||
Download anime to watch later dub or sub with this one command.
|
||||
Its optimized for scripting due to fuzzy matching.
|
||||
So every step of the way has been and can be automated.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The download feature is powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp) so all the bells and whistles that it provides are readily available in the project.
|
||||
> Like continuing from where you left of while downloading, after lets say you lost your internet connection.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# Download all available episodes
|
||||
fastanime download <anime-title>
|
||||
|
||||
# Download specific episode range
|
||||
# be sure to observe the range Syntax
|
||||
fastanime download <anime-title> -r <episodes-start>-<episodes-end>
|
||||
```
|
||||
|
||||
#### search subcommand
|
||||
|
||||
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# basic form where you will still be promted for the episode number
|
||||
fastanime search <anime-title>
|
||||
|
||||
# binge all episodes with this command
|
||||
fastanime search <anime-title> -
|
||||
|
||||
# binge a specific episode range with this command
|
||||
# be sure to observe the range Syntax
|
||||
fastanime search <anime-title> <episodes-start>-<episodes-end>
|
||||
```
|
||||
|
||||
#### downloads subcommand
|
||||
|
||||
View and stream the anime you downloaded using MPV.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
fastanime downloads
|
||||
|
||||
# to get the path to the downloads folder set
|
||||
fastanime downloads --path
|
||||
# useful when you want to use the value for other programs
|
||||
```
|
||||
|
||||
#### config subcommand
|
||||
|
||||
Edit FastAnime configuration settings using your preferred editor (based on `$EDITOR` environment variable so be sure to set it).
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
fastanime config
|
||||
|
||||
# to get config path which is useful if you want to use it for another program.
|
||||
fastanime config --path
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` in case you don't know.
|
||||
|
||||
## Configuration
|
||||
|
||||
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on Linux and mac or somewhere on windows; you can check by running `fastanime config --path`.
|
||||
|
||||
```ini
|
||||
[stream]
|
||||
continue_from_history = True # Auto continue from watch history
|
||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
auto_next = False # Auto-select next episode
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select=True
|
||||
# whether to skip the opening and ending theme songs
|
||||
# note requires ani-skip to be in path
|
||||
skip=false
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
# learn more by looking it up on their site
|
||||
# only works for downloaded anime if server=gogoanime
|
||||
# since its the only one that offers different formats
|
||||
# the others tend not to
|
||||
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
|
||||
[general]
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
||||
preview=false # whether to show a preview window when using fzf
|
||||
|
||||
# whether to show the icons
|
||||
icons=false
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
notification_duration=2
|
||||
|
||||
[anilist]
|
||||
# Not implemented yet
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
|
||||
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed.
|
||||
|
||||
## Receiving Support
|
||||
|
||||
For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/HRjySFjQ">
|
||||
<img src="https://invidget.switchblade.xyz/HRjySFjQ">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Supporting the Project
|
||||
|
||||
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).
|
||||
|
||||
4
anixstream/.vscode/settings.json
vendored
4
anixstream/.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
from .home_screen import HomeScreenController
|
||||
from .search_screen import SearchScreenController
|
||||
from .my_list_screen import MyListScreenController
|
||||
from .anime_screen import AnimeScreenController
|
||||
from .downloads_screen import DownloadsScreenController
|
||||
from .help_screen import HelpScreenController
|
||||
from .crashlog_screen import CrashLogScreenController
|
||||
@@ -1,43 +0,0 @@
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.cache import Cache
|
||||
|
||||
from anixstream.Model import AnimeScreenModel
|
||||
from anixstream.View import AnimeScreenView
|
||||
|
||||
Cache.register("data.anime", limit=20, timeout=600)
|
||||
|
||||
|
||||
class AnimeScreenController:
|
||||
"""The controller for the anime screen
|
||||
"""
|
||||
def __init__(self, model: AnimeScreenModel):
|
||||
self.model = model
|
||||
self.view = AnimeScreenView(controller=self, model=self.model)
|
||||
|
||||
def get_view(self) -> AnimeScreenView:
|
||||
return self.view
|
||||
|
||||
def update_anime_view(self, id: int, caller_screen_name: str):
|
||||
"""method called to update the anime screen when a new
|
||||
|
||||
Args:
|
||||
id (int): the anilst id of the anime
|
||||
caller_screen_name (str): the screen thats calling this method; used internally to switch back to this screen
|
||||
"""
|
||||
if self.model.anime_id != id:
|
||||
if cached_anime_data := Cache.get("data.anime", f"{id}"):
|
||||
data = cached_anime_data
|
||||
else:
|
||||
data = self.model.get_anime_data(id)
|
||||
|
||||
if data[0]:
|
||||
|
||||
self.model.anime_id = id
|
||||
Clock.schedule_once(
|
||||
lambda _: self.view.update_layout(
|
||||
data[1]["data"]["Page"]["media"][0], caller_screen_name
|
||||
)
|
||||
)
|
||||
Logger.info(f"Anime Screen:Success in opening anime of id: {id}")
|
||||
Cache.append("data.anime", f"{id}", data)
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
from anixstream.View import CrashLogScreenView
|
||||
from anixstream.Model import CrashLogScreenModel
|
||||
|
||||
|
||||
|
||||
class CrashLogScreenController:
|
||||
"""The crash log screen controller
|
||||
"""
|
||||
def __init__(self, model:CrashLogScreenModel):
|
||||
self.model = model
|
||||
self.view = CrashLogScreenView(controller=self, model=self.model)
|
||||
# self.update_anime_view()
|
||||
|
||||
def get_view(self) -> CrashLogScreenView:
|
||||
return self.view
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
from anixstream.View import DownloadsScreenView
|
||||
from anixstream.Model import DownloadsScreenModel
|
||||
|
||||
|
||||
class DownloadsScreenController:
|
||||
"""The controller for the download screen
|
||||
"""
|
||||
def __init__(self, model:DownloadsScreenModel):
|
||||
self.model = model
|
||||
self.view = DownloadsScreenView(controller=self, model=self.model)
|
||||
|
||||
def get_view(self) -> DownloadsScreenView:
|
||||
return self.view
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
from anixstream.View import HelpScreenView
|
||||
from anixstream.Model import HelpScreenModel
|
||||
|
||||
|
||||
class HelpScreenController:
|
||||
"""The help screen controller
|
||||
"""
|
||||
def __init__(self, model:HelpScreenModel):
|
||||
self.model = model
|
||||
self.view = HelpScreenView(controller=self, model=self.model)
|
||||
|
||||
def get_view(self) -> HelpScreenView:
|
||||
return self.view
|
||||
@@ -1,121 +0,0 @@
|
||||
|
||||
from inspect import isgenerator
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
|
||||
from anixstream.View import HomeScreenView
|
||||
from anixstream.Model import HomeScreenModel
|
||||
from anixstream.View.components import MediaCardsContainer
|
||||
from anixstream.Utility import show_notification
|
||||
|
||||
|
||||
# TODO:Move the update home screen to homescreen.py
|
||||
class HomeScreenController:
|
||||
"""
|
||||
The `HomeScreenController` class represents a controller implementation.
|
||||
Coordinates work of the view with the model.
|
||||
The controller implements the strategy pattern. The controller connects to
|
||||
the view to control its actions.
|
||||
"""
|
||||
populate_errors = []
|
||||
|
||||
def __init__(self, model:HomeScreenModel):
|
||||
self.model = model # Model.main_screen.MainScreenModel
|
||||
self.view = HomeScreenView(controller=self, model=self.model)
|
||||
# if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
|
||||
# Clock.schedule_once(lambda _:self.populate_home_screen())
|
||||
|
||||
def get_view(self) -> HomeScreenView:
|
||||
return self.view
|
||||
|
||||
def popular_anime(self):
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load most popular anime")
|
||||
self.populate_errors.append("Most Popular Anime")
|
||||
|
||||
def favourite_anime(self):
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load most favourite anime")
|
||||
self.populate_errors.append("Most favourite Anime")
|
||||
|
||||
def trending_anime(self):
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load trending anime")
|
||||
self.populate_errors.append("trending Anime")
|
||||
|
||||
def highest_scored_anime(self):
|
||||
most_scored_cards_container = MediaCardsContainer()
|
||||
most_scored_cards_container.list_name = "Most Scored"
|
||||
most_scored_cards_generator = self.model.get_most_scored_anime()
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load highest scored anime")
|
||||
self.populate_errors.append("Most scored Anime")
|
||||
|
||||
def recently_updated_anime(self):
|
||||
most_recently_updated_cards_container = MediaCardsContainer()
|
||||
most_recently_updated_cards_container.list_name = "Most Recently Updated"
|
||||
most_recently_updated_cards_generator = self.model.get_most_recently_updated_anime()
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load recently updated anime")
|
||||
self.populate_errors.append("Most recently updated Anime")
|
||||
|
||||
def upcoming_anime(self):
|
||||
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:
|
||||
Logger.error("Home Screen:Failed to load upcoming anime")
|
||||
self.populate_errors.append("upcoming Anime")
|
||||
|
||||
def populate_home_screen(self):
|
||||
self.populate_errors = []
|
||||
Clock.schedule_once(lambda _:self.trending_anime(),1)
|
||||
Clock.schedule_once(lambda _:self.highest_scored_anime(),2)
|
||||
Clock.schedule_once(lambda _:self.popular_anime(),3)
|
||||
Clock.schedule_once(lambda _: self.favourite_anime(),4)
|
||||
Clock.schedule_once(lambda _:self.recently_updated_anime(),5)
|
||||
Clock.schedule_once(lambda _:self.upcoming_anime(),6)
|
||||
|
||||
if self.populate_errors:
|
||||
show_notification(f"Failed to fetch all home screen data",f"Theres probably a problem with your internet connection or anilist servers are down.\nFailed include:{', '.join(self.populate_errors)}")
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from inspect import isgenerator
|
||||
|
||||
from kivy.logger import Logger
|
||||
# from kivy.clock import Clock
|
||||
from kivy.utils import difference
|
||||
|
||||
from anixstream.View import MyListScreenView
|
||||
from anixstream.Model import MyListScreenModel
|
||||
from anixstream.Utility import user_data_helper
|
||||
|
||||
class MyListScreenController:
|
||||
"""
|
||||
The `MyListScreenController` class represents a controller implementation.
|
||||
Coordinates work of the view with the model.
|
||||
The controller implements the strategy pattern. The controller connects to
|
||||
the view to control its actions.
|
||||
"""
|
||||
|
||||
def __init__(self, model:MyListScreenModel):
|
||||
self.model = model
|
||||
self.view = MyListScreenView(controller=self, model=self.model)
|
||||
if len(self.requested_update_my_list_screen())>30:
|
||||
self.requested_update_my_list_screen(2)
|
||||
|
||||
def get_view(self) -> MyListScreenView:
|
||||
return self.view
|
||||
|
||||
def requested_update_my_list_screen(self,page=None):
|
||||
user_anime_list = user_data_helper.get_user_anime_list()
|
||||
if animes_to_add:=difference(user_anime_list,self.model.already_in_user_anime_list):
|
||||
Logger.info("My List Screen:User anime list change;updating screen")
|
||||
# if thirty:=len(animes_to_add)>30:
|
||||
# self.model.already_in_user_anime_list = user_anime_list[:30]
|
||||
# else:
|
||||
|
||||
anime_cards = self.model.update_my_anime_list_view(animes_to_add,page)
|
||||
self.model.already_in_user_anime_list = user_anime_list
|
||||
|
||||
if isgenerator(anime_cards):
|
||||
for result_card in anime_cards:
|
||||
result_card.screen = self.view
|
||||
self.view.update_layout(result_card)
|
||||
return animes_to_add
|
||||
elif page:
|
||||
anime_cards = self.model.update_my_anime_list_view(self.model.already_in_user_anime_list,page)
|
||||
# self.model.already_in_user_anime_list = user_anime_list
|
||||
if isgenerator(anime_cards):
|
||||
for result_card in anime_cards:
|
||||
result_card.screen = self.view
|
||||
self.view.update_layout(result_card)
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
@@ -1,47 +0,0 @@
|
||||
from inspect import isgenerator
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
|
||||
from anixstream.View import SearchScreenView
|
||||
from anixstream.Model import SearchScreenModel
|
||||
|
||||
|
||||
class SearchScreenController:
|
||||
"""The search screen controller
|
||||
"""
|
||||
|
||||
def __init__(self, model: SearchScreenModel):
|
||||
self.model = model
|
||||
self.view = SearchScreenView(controller=self, model=self.model)
|
||||
|
||||
def get_view(self) -> SearchScreenView:
|
||||
return self.view
|
||||
|
||||
def update_trending_anime(self):
|
||||
"""Gets and adds the trending anime to the search screen
|
||||
"""
|
||||
trending_cards_generator = self.model.get_trending_anime()
|
||||
if isgenerator(trending_cards_generator):
|
||||
self.view.trending_anime_sidebar.clear_widgets()
|
||||
for card in trending_cards_generator:
|
||||
card.screen = self.view
|
||||
card.pos_hint = {"center_x": 0.5}
|
||||
self.view.update_trending_sidebar(card)
|
||||
else:
|
||||
Logger.error("Home Screen:Failed to load trending anime")
|
||||
|
||||
def requested_search_for_anime(self, anime_title, **kwargs):
|
||||
self.view.is_searching = True
|
||||
search_Results = self.model.search_for_anime(anime_title, **kwargs)
|
||||
if isgenerator(search_Results):
|
||||
for result_card in search_Results:
|
||||
result_card.screen = self.view
|
||||
self.view.update_layout(result_card)
|
||||
Clock.schedule_once(
|
||||
lambda _: self.view.update_pagination(self.model.pagination_info)
|
||||
)
|
||||
self.update_trending_anime()
|
||||
else:
|
||||
Logger.error(f"Home Screen:Failed to search for {anime_title}")
|
||||
self.view.is_searching = False
|
||||
@@ -1,7 +0,0 @@
|
||||
from .home_screen import HomeScreenModel
|
||||
from .search_screen import SearchScreenModel
|
||||
from .my_list_screen import MyListScreenModel
|
||||
from .anime_screen import AnimeScreenModel
|
||||
from .crashlog_screen import CrashLogScreenModel
|
||||
from .help_screen import HelpScreenModel
|
||||
from .download_screen import DownloadsScreenModel
|
||||
@@ -1,13 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
from anixstream.libs.anilist import AniList
|
||||
|
||||
class AnimeScreenModel(BaseScreenModel):
|
||||
"""the Anime screen model
|
||||
"""
|
||||
data = {}
|
||||
anime_id = 0
|
||||
|
||||
def get_anime_data(self,id:int):
|
||||
return AniList.get_anime(id)
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,7 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
|
||||
|
||||
class CrashLogScreenModel(BaseScreenModel):
|
||||
"""
|
||||
Handles the crashlog screen logic
|
||||
"""
|
||||
@@ -1,8 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
|
||||
|
||||
class DownloadsScreenModel(BaseScreenModel):
|
||||
"""
|
||||
Handles the download screen logic
|
||||
"""
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
|
||||
|
||||
|
||||
class HelpScreenModel(BaseScreenModel):
|
||||
"""
|
||||
Handles the help screen logic
|
||||
"""
|
||||
@@ -1,79 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
from anixstream.libs.anilist import AniList
|
||||
from anixstream.Utility.media_card_loader import MediaCardLoader
|
||||
|
||||
|
||||
class HomeScreenModel(BaseScreenModel):
|
||||
"""The home screen model"""
|
||||
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
from anixstream.libs.anilist import AniList
|
||||
from .base_model import BaseScreenModel
|
||||
from anixstream.Utility import MediaCardLoader,show_notification
|
||||
|
||||
|
||||
class MyListScreenModel(BaseScreenModel):
|
||||
already_in_user_anime_list = []
|
||||
def update_my_anime_list_view(self,not_yet_in_user_anime_list:list,page=None):
|
||||
success,self.data = AniList.search(id_in=not_yet_in_user_anime_list,page=page,sort="SCORE_DESC")
|
||||
if success:
|
||||
return self.media_card_generator()
|
||||
else:
|
||||
show_notification(f"Failed to update my list screen view",self.data["Error"])
|
||||
return None
|
||||
|
||||
def media_card_generator(self):
|
||||
for anime_item in self.data["data"]["Page"]["media"]:
|
||||
yield MediaCardLoader.media_card(anime_item)
|
||||
@@ -1,29 +0,0 @@
|
||||
from .base_model import BaseScreenModel
|
||||
from anixstream.libs.anilist import AniList
|
||||
from anixstream.Utility import MediaCardLoader, show_notification
|
||||
|
||||
|
||||
class SearchScreenModel(BaseScreenModel):
|
||||
data = {}
|
||||
|
||||
def get_trending_anime(self):
|
||||
success,data = AniList.get_trending()
|
||||
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 search_for_anime(self,anime_title,**kwargs):
|
||||
success,self.data = AniList.search(query=anime_title,**kwargs)
|
||||
if success:
|
||||
return self.media_card_generator()
|
||||
else:
|
||||
show_notification(f"Failed to search for {anime_title}",self.data.get("Error"))
|
||||
|
||||
def media_card_generator(self):
|
||||
for anime_item in self.data["data"]["Page"]["media"]:
|
||||
yield MediaCardLoader.media_card(anime_item)
|
||||
self.pagination_info = self.data["data"]["Page"]["pageInfo"]
|
||||
@@ -1,4 +0,0 @@
|
||||
from .media_card_loader import MediaCardLoader
|
||||
from .show_notification import show_notification
|
||||
from .data import themes_available
|
||||
from .utils import write_crash
|
||||
@@ -1,41 +0,0 @@
|
||||
from typing import TypedDict
|
||||
import plyer
|
||||
import os
|
||||
|
||||
from .yaml_parser import YamlParser
|
||||
|
||||
|
||||
class AnimdlConfig(TypedDict):
|
||||
default_player: str
|
||||
default_provider: str
|
||||
quality_string: str
|
||||
|
||||
|
||||
if local_data_path:=os.getenv("LOCALAPPDATA"):
|
||||
config_dir = os.path.join(local_data_path,".config")
|
||||
if not os.path.exists(config_dir):
|
||||
os.mkdir(config_dir)
|
||||
animdl_config_folder_location = os.path.join(config_dir, "animdl")
|
||||
else:
|
||||
user_profile_path = plyer.storagepath.get_home_dir() # type: ignore
|
||||
animdl_config_folder_location = os.path.join(user_profile_path, ".animdl")
|
||||
if not os.path.exists(animdl_config_folder_location):
|
||||
os.mkdir(animdl_config_folder_location)
|
||||
|
||||
animdl_config_location = os.path.join(animdl_config_folder_location, "config.yml")
|
||||
# print(animdl_config_location)
|
||||
animdl_config = YamlParser(
|
||||
animdl_config_location,
|
||||
{"default_player": "mpv", "default_provider": "allanime", "quality_string": "best"},
|
||||
AnimdlConfig,
|
||||
)
|
||||
|
||||
|
||||
def update_animdl_config(field_to_update: str, value):
|
||||
current_data = animdl_config.data
|
||||
current_data[f"{field_to_update}"] = value
|
||||
animdl_config.write(current_data)
|
||||
|
||||
|
||||
def get_animdl_config() -> AnimdlConfig:
|
||||
return animdl_config.data
|
||||
@@ -1,6 +0,0 @@
|
||||
"""
|
||||
Just contains some useful data used across the codebase
|
||||
"""
|
||||
|
||||
themes_available = ['Aliceblue', 'Antiquewhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'Blanchedalmond', 'Blue', 'Blueviolet', 'Brown', 'Burlywood', 'Cadetblue', 'Chartreuse', 'Chocolate', 'Coral', 'Cornflowerblue', 'Cornsilk', 'Crimson', 'Cyan', 'Darkblue', 'Darkcyan', 'Darkgoldenrod', 'Darkgray', 'Darkgrey', 'Darkgreen', 'Darkkhaki', 'Darkmagenta', 'Darkolivegreen', 'Darkorange', 'Darkorchid', 'Darkred', 'Darksalmon', 'Darkseagreen', 'Darkslateblue', 'Darkslategray', 'Darkslategrey', 'Darkturquoise', 'Darkviolet', 'Deeppink', 'Deepskyblue', 'Dimgray', 'Dimgrey', 'Dodgerblue', 'Firebrick', 'Floralwhite', 'Forestgreen', 'Fuchsia', 'Gainsboro', 'Ghostwhite', 'Gold', 'Goldenrod', 'Gray', 'Grey', 'Green', 'Greenyellow', 'Honeydew', 'Hotpink', 'Indianred', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'Lavenderblush', 'Lawngreen', 'Lemonchiffon', 'Lightblue', 'Lightcoral', 'Lightcyan', 'Lightgoldenrodyellow', 'Lightgreen', 'Lightgray', 'Lightgrey', 'Lightpink', 'Lightsalmon', 'Lightseagreen', 'Lightskyblue', 'Lightslategray', 'Lightslategrey', 'Lightsteelblue', 'Lightyellow', 'Lime', 'Limegreen', 'Linen', 'Magenta', 'Maroon', 'Mediumaquamarine', 'Mediumblue', 'Mediumorchid', 'Mediumpurple', 'Mediumseagreen', 'Mediumslateblue', 'Mediumspringgreen', 'Mediumturquoise', 'Mediumvioletred', 'Midnightblue', 'Mintcream', 'Mistyrose', 'Moccasin', 'Navajowhite', 'Navy', 'Oldlace', 'Olive', 'Olivedrab', 'Orange', 'Orangered', 'Orchid', 'Palegoldenrod', 'Palegreen', 'Paleturquoise', 'Palevioletred', 'Papayawhip', 'Peachpuff', 'Peru', 'Pink', 'Plum', 'Powderblue', 'Purple', 'Red', 'Rosybrown', 'Royalblue', 'Saddlebrown', 'Salmon', 'Sandybrown', 'Seagreen', 'Seashell', 'Sienna', 'Silver', 'Skyblue', 'Slateblue', 'Slategray', 'Slategrey', 'Snow', 'Springgreen', 'Steelblue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'Whitesmoke', 'Yellow',
|
||||
'Yellowgreen']
|
||||
@@ -1,54 +0,0 @@
|
||||
"""
|
||||
Contains helper functions to make your life easy when adding kivy markup to text
|
||||
"""
|
||||
|
||||
from kivy.utils import get_hex_from_color
|
||||
|
||||
|
||||
def bolden(text: str):
|
||||
return f"[b]{text}[/b]"
|
||||
|
||||
|
||||
def italicize(text: str):
|
||||
return f"[i]{text}[/i]"
|
||||
|
||||
|
||||
def underline(text: str):
|
||||
return f"[u]{text}[/u]"
|
||||
|
||||
|
||||
def strike_through(text: str):
|
||||
return f"[s]{text}[/s]"
|
||||
|
||||
|
||||
def sub_script(text: str):
|
||||
return f"[sub]{text}[/sub]"
|
||||
|
||||
|
||||
def super_script(text: str):
|
||||
return f"[sup]{text}[/sup]"
|
||||
|
||||
|
||||
def color_text(text: str, color: tuple):
|
||||
hex_color = get_hex_from_color(color)
|
||||
return f"[color={hex_color}]{text}[/color]"
|
||||
|
||||
|
||||
def font(text: str, font_name: str):
|
||||
return f"[font={font_name}]{text}[/font]"
|
||||
|
||||
|
||||
def font_family(text: str, family: str):
|
||||
return f"[font_family={family}]{text}[/font_family]"
|
||||
|
||||
|
||||
def font_context(text: str, context: str):
|
||||
return f"[font_context={context}]{text}[/font_context]"
|
||||
|
||||
|
||||
def font_size(text: str, size: int):
|
||||
return f"[size={size}]{text}[/size]"
|
||||
|
||||
|
||||
def text_ref(text: str, ref: str):
|
||||
return f"[ref={ref}]{text}[/ref]"
|
||||
@@ -1,276 +0,0 @@
|
||||
from collections import deque
|
||||
from time import sleep
|
||||
import threading
|
||||
|
||||
from pytube import YouTube
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.cache import Cache
|
||||
from kivy.loader import _ThreadPool
|
||||
from kivy.logger import Logger
|
||||
|
||||
from anixstream.View.components import MediaCard
|
||||
from anixstream.Utility import anilist_data_helper, user_data_helper
|
||||
from anixstream.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
|
||||
|
||||
# Register anime cache in memory
|
||||
Cache.register("yt_stream_links.anime")
|
||||
|
||||
user_anime_list = user_data_helper.get_user_anime_list()
|
||||
|
||||
yt_stream_links = user_data_helper.get_anime_trailer_cache()
|
||||
for link in yt_stream_links:
|
||||
Cache.append("yt_stream_links.anime", link[0], tuple(link[1]))
|
||||
|
||||
|
||||
# TODO: Make this process more efficient
|
||||
# for youtube video links gotten from from pytube which is blocking
|
||||
class MediaCardDataLoader(object):
|
||||
"""this class loads an anime media card and gets the trailer url from pytube"""
|
||||
|
||||
def __init__(self):
|
||||
self._resume_cond = threading.Condition()
|
||||
self._num_workers = 5
|
||||
self._max_upload_per_frame = 5
|
||||
self._paused = False
|
||||
self._q_load = deque()
|
||||
self._q_done = deque()
|
||||
self._client = []
|
||||
self._running = False
|
||||
self._start_wanted = False
|
||||
self._trigger_update = Clock.create_trigger(self._update)
|
||||
|
||||
def start(self):
|
||||
"""Start the loader thread/process."""
|
||||
self._running = True
|
||||
|
||||
def run(self, *largs):
|
||||
"""Main loop for the loader."""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Stop the loader thread/process."""
|
||||
self._running = False
|
||||
|
||||
def pause(self):
|
||||
"""Pause the loader, can be useful during interactions.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
"""
|
||||
self._paused = True
|
||||
|
||||
def resume(self):
|
||||
"""Resume the loader, after a :meth:`pause`.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
"""
|
||||
self._paused = False
|
||||
self._resume_cond.acquire()
|
||||
self._resume_cond.notify_all()
|
||||
self._resume_cond.release()
|
||||
|
||||
def _wait_for_resume(self):
|
||||
while self._running and self._paused:
|
||||
self._resume_cond.acquire()
|
||||
self._resume_cond.wait(0.25)
|
||||
self._resume_cond.release()
|
||||
|
||||
def cached_fetch_data(self, yt_watch_url):
|
||||
data: tuple = Cache.get("yt_stream_links.anime", yt_watch_url) # type: ignore # trailer_url is the yt_watch_link
|
||||
|
||||
if not data[0]:
|
||||
yt = YouTube(yt_watch_url)
|
||||
preview_image = yt.thumbnail_url
|
||||
try:
|
||||
video_stream_url = yt.streams.filter(
|
||||
progressive=True, file_extension="mp4"
|
||||
)[-1].url
|
||||
data = preview_image, video_stream_url
|
||||
yt_stream_links.append((yt_watch_url, data))
|
||||
user_data_helper.update_anime_trailer_cache(yt_stream_links)
|
||||
except:
|
||||
data = preview_image, None
|
||||
return data
|
||||
|
||||
def _load(self, kwargs):
|
||||
while len(self._q_done) >= (self._max_upload_per_frame * self._num_workers):
|
||||
sleep(0.1) # type: ignore
|
||||
self._wait_for_resume()
|
||||
yt_watch_link = kwargs["yt_watch_link"]
|
||||
try:
|
||||
data = self.cached_fetch_data(yt_watch_link)
|
||||
except Exception as e:
|
||||
data = None
|
||||
Logger.error("Pytube:{e}")
|
||||
|
||||
self._q_done.appendleft((yt_watch_link, data))
|
||||
self._trigger_update()
|
||||
|
||||
def _update(self, *largs):
|
||||
if self._start_wanted:
|
||||
if not self._running:
|
||||
self.start()
|
||||
self._start_wanted = False
|
||||
|
||||
# in pause mode, don't unqueue anything.
|
||||
if self._paused:
|
||||
self._trigger_update()
|
||||
return
|
||||
|
||||
for _ in range(self._max_upload_per_frame):
|
||||
try:
|
||||
yt_watch_link, data = self._q_done.pop()
|
||||
except IndexError:
|
||||
return
|
||||
# update client
|
||||
for c_yt_watch_link, client in self._client[:]:
|
||||
if yt_watch_link != c_yt_watch_link:
|
||||
continue
|
||||
|
||||
# got one client to update
|
||||
if data:
|
||||
trailer_url = data[1]
|
||||
if trailer_url:
|
||||
client.set_trailer_url(trailer_url)
|
||||
Logger.info(f"Pytube:Found trailer url for {client.title}")
|
||||
Cache.append("yt_stream_links.anime", yt_watch_link, data)
|
||||
self._client.remove((c_yt_watch_link, client))
|
||||
|
||||
self._trigger_update()
|
||||
|
||||
def media_card(
|
||||
self,
|
||||
anime_item: AnilistBaseMediaDataSchema,
|
||||
load_callback=None,
|
||||
post_callback=None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
media_card = MediaCard()
|
||||
media_card.anime_id = anime_id = anime_item["id"]
|
||||
|
||||
# TODO: ADD language preference
|
||||
if anime_item["title"].get("english"):
|
||||
media_card.title = anime_item["title"]["english"]
|
||||
else:
|
||||
media_card.title = anime_item["title"]["romaji"]
|
||||
|
||||
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"]
|
||||
else:
|
||||
media_card.description = "None"
|
||||
|
||||
# TODO: switch to season and year
|
||||
media_card.first_aired_on = (
|
||||
f'{anilist_data_helper.format_anilist_date_object(anime_item["startDate"])}'
|
||||
)
|
||||
|
||||
media_card.studios = anilist_data_helper.format_list_data_with_comma(
|
||||
[
|
||||
studio["name"]
|
||||
for studio in anime_item["studios"]["nodes"]
|
||||
if studio["isAnimationStudio"]
|
||||
]
|
||||
)
|
||||
|
||||
media_card.producers = anilist_data_helper.format_list_data_with_comma(
|
||||
[
|
||||
studio["name"]
|
||||
for studio in anime_item["studios"]["nodes"]
|
||||
if not studio["isAnimationStudio"]
|
||||
]
|
||||
)
|
||||
|
||||
media_card.next_airing_episode = "{}".format(
|
||||
anilist_data_helper.extract_next_airing_episode(
|
||||
anime_item["nextAiringEpisode"]
|
||||
)
|
||||
)
|
||||
if anime_item.get("tags"):
|
||||
media_card.tags = anilist_data_helper.format_list_data_with_comma(
|
||||
[tag["name"] for tag in anime_item["tags"]]
|
||||
)
|
||||
|
||||
media_card.media_status = anime_item["status"]
|
||||
|
||||
if anime_item.get("genres"):
|
||||
media_card.genres = anilist_data_helper.format_list_data_with_comma(
|
||||
anime_item["genres"]
|
||||
)
|
||||
|
||||
if anime_id in user_anime_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
|
||||
|
||||
# Setting up trailer info to be gotten if available
|
||||
if anime_item["trailer"]:
|
||||
yt_watch_link = "https://youtube.com/watch?v=" + anime_item["trailer"]["id"]
|
||||
data = Cache.get("yt_stream_links.anime", yt_watch_link) # type: ignore # trailer_url is the yt_watch_link
|
||||
if data:
|
||||
if data[1] not in (None, False):
|
||||
media_card.set_preview_image(data[0])
|
||||
media_card.set_trailer_url(data[1])
|
||||
return media_card
|
||||
else:
|
||||
# if data is None, this is really the first time
|
||||
self._client.append((yt_watch_link, media_card))
|
||||
self._q_load.appendleft(
|
||||
{
|
||||
"yt_watch_link": yt_watch_link,
|
||||
"load_callback": load_callback,
|
||||
"post_callback": post_callback,
|
||||
"current_anime": anime_item["id"],
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
)
|
||||
if not kwargs.get("nocache", False):
|
||||
Cache.append("yt_stream_links.anime", yt_watch_link, (False, False))
|
||||
self._start_wanted = True
|
||||
self._trigger_update()
|
||||
return media_card
|
||||
|
||||
|
||||
class LoaderThreadPool(MediaCardDataLoader):
|
||||
def __init__(self):
|
||||
super(LoaderThreadPool, self).__init__()
|
||||
self.pool: _ThreadPool | None = None
|
||||
|
||||
def start(self):
|
||||
super(LoaderThreadPool, self).start()
|
||||
self.pool = _ThreadPool(self._num_workers)
|
||||
Clock.schedule_interval(self.run, 0)
|
||||
|
||||
def stop(self):
|
||||
super(LoaderThreadPool, self).stop()
|
||||
Clock.unschedule(self.run)
|
||||
self.pool.stop() # type: ignore
|
||||
|
||||
def run(self, *largs):
|
||||
while self._running:
|
||||
try:
|
||||
parameters = self._q_load.pop()
|
||||
except:
|
||||
return
|
||||
self.pool.add_task(self._load, parameters) # type: ignore
|
||||
|
||||
|
||||
MediaCardLoader = LoaderThreadPool()
|
||||
Logger.info(
|
||||
"MediaCardLoader: using a thread pool of {} workers".format(
|
||||
MediaCardLoader._num_workers
|
||||
)
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
# 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.
|
||||
"""
|
||||
@@ -1,29 +0,0 @@
|
||||
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText, MDSnackbarSupportingText
|
||||
from kivy.clock import Clock
|
||||
|
||||
|
||||
def show_notification(title, details):
|
||||
"""helper function to display notifications
|
||||
|
||||
Args:
|
||||
title (str): the title of your message
|
||||
details (str): the details of your message
|
||||
"""
|
||||
|
||||
def _show(dt):
|
||||
MDSnackbar(
|
||||
MDSnackbarText(
|
||||
text=title,
|
||||
adaptive_height=True,
|
||||
),
|
||||
MDSnackbarSupportingText(
|
||||
text=details, shorten=False, max_lines=0, adaptive_height=True
|
||||
),
|
||||
duration=5,
|
||||
y="10dp",
|
||||
pos_hint={"bottom": 1, "right": 0.99},
|
||||
padding=[0, 0, "8dp", "8dp"],
|
||||
size_hint_x=0.4,
|
||||
).open()
|
||||
|
||||
Clock.schedule_once(_show, 1)
|
||||
@@ -1,82 +0,0 @@
|
||||
"""
|
||||
Contains Helper functions to read and write the user data files
|
||||
"""
|
||||
|
||||
from kivy.storage.jsonstore import JsonStore
|
||||
from datetime import date, datetime
|
||||
from kivy.logger import Logger
|
||||
from kivy.resources import resource_find
|
||||
|
||||
today = date.today()
|
||||
now = datetime.now()
|
||||
|
||||
user_data = JsonStore(resource_find("user_data.json"))
|
||||
yt_cache = JsonStore(resource_find("yt_cache.json"))
|
||||
|
||||
|
||||
# Get the user data
|
||||
def get_user_anime_list() -> list:
|
||||
try:
|
||||
return user_data.get("user_anime_list")[
|
||||
"user_anime_list"
|
||||
] # returns a list of anime ids
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Read failure:{e}")
|
||||
return []
|
||||
|
||||
|
||||
def update_user_anime_list(updated_list: list):
|
||||
try:
|
||||
updated_list_ = list(set(updated_list))
|
||||
user_data.put("user_anime_list", user_anime_list=updated_list_)
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Update failure:{e}")
|
||||
|
||||
|
||||
# Get the user data
|
||||
def get_user_downloads() -> list:
|
||||
try:
|
||||
return user_data.get("user_downloads")[
|
||||
"user_downloads"
|
||||
] # returns a list of anime ids
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Read failure:{e}")
|
||||
return []
|
||||
|
||||
|
||||
def update_user_downloads(updated_list: list):
|
||||
try:
|
||||
user_data.put("user_downloads", user_downloads=list(set(updated_list)))
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Update failure:{e}")
|
||||
|
||||
|
||||
# Yt persistent anime trailer cache
|
||||
t = 1
|
||||
if now.hour <= 6:
|
||||
t = 1
|
||||
elif now.hour <= 12:
|
||||
t = 2
|
||||
elif now.hour <= 18:
|
||||
t = 3
|
||||
else:
|
||||
t = 4
|
||||
|
||||
yt_anime_trailer_cache_name = f"{today}{t}"
|
||||
|
||||
|
||||
def get_anime_trailer_cache() -> list:
|
||||
try:
|
||||
return yt_cache["yt_stream_links"][f"{yt_anime_trailer_cache_name}"]
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Read failure:{e}")
|
||||
return []
|
||||
|
||||
|
||||
def update_anime_trailer_cache(yt_stream_links: list):
|
||||
try:
|
||||
yt_cache.put(
|
||||
"yt_stream_links", **{f"{yt_anime_trailer_cache_name}": yt_stream_links}
|
||||
)
|
||||
except Exception as e:
|
||||
Logger.warning(f"User Data:Update failure:{e}")
|
||||
@@ -1,34 +0,0 @@
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
import os
|
||||
# TODO: make it use color_text instead of fixed vals
|
||||
# from .kivy_markup_helper import color_text
|
||||
|
||||
|
||||
# utility functions
|
||||
def write_crash(e: Exception):
|
||||
index = datetime.today()
|
||||
error = f"[b][color=#fa0000][ {index} ]:[/color][/b]\n(\n\n{e}\n\n)\n"
|
||||
try:
|
||||
with open("crashdump.txt", "a") as file:
|
||||
file.write(error)
|
||||
except:
|
||||
with open("crashdump.txt", "w") as file:
|
||||
file.write(error)
|
||||
return index
|
||||
|
||||
|
||||
def read_crash_file():
|
||||
crash_file_path = "./crashfile.txt"
|
||||
if not os.path.exists(crash_file_path):
|
||||
return None
|
||||
else:
|
||||
with open(crash_file_path,"r") as file:
|
||||
return file.read()
|
||||
|
||||
def move_file(source_path, dest_path):
|
||||
try:
|
||||
path = shutil.move(source_path, dest_path)
|
||||
return (1, path)
|
||||
except Exception as e:
|
||||
return (0, e)
|
||||
@@ -1,34 +0,0 @@
|
||||
import yaml
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class YamlParser:
|
||||
"""makes managing yaml files easier"""
|
||||
|
||||
data = {}
|
||||
|
||||
def __init__(self, file_path: str, default, data_type):
|
||||
self.file_path: str = file_path
|
||||
self.data: data_type
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
with open(self.file_path, "r") as yaml_file:
|
||||
self.data = yaml.safe_load(yaml_file)
|
||||
except:
|
||||
self.data = default
|
||||
with open(file_path, "w") as yaml_file:
|
||||
yaml.dump(default, yaml_file)
|
||||
else:
|
||||
self.data = default
|
||||
with open(file_path, "w") as yaml_file:
|
||||
yaml.dump(default, yaml_file)
|
||||
|
||||
def read(self):
|
||||
with open(self.file_path, "r") as yaml_file:
|
||||
self.data = yaml.safe_load(yaml_file)
|
||||
return self.data
|
||||
|
||||
def write(self, new_obj):
|
||||
with open(self.file_path, "w") as yaml_file:
|
||||
yaml.dump(new_obj, yaml_file)
|
||||
@@ -1,61 +0,0 @@
|
||||
<AnimeScreenView>:
|
||||
md_bg_color: self.theme_cls.backgroundColor
|
||||
header:header
|
||||
side_bar:side_bar
|
||||
rankings_bar:rankings_bar
|
||||
anime_description:anime_description
|
||||
anime_characters:anime_characters
|
||||
anime_reviews:anime_reviews
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint_y:None
|
||||
height: self.minimum_height
|
||||
MDBoxLayout:
|
||||
adaptive_height:True
|
||||
MDIconButton:
|
||||
icon: "arrow-left"
|
||||
on_release:
|
||||
root.manager_screens.current = root.caller_screen_name
|
||||
MDScrollView:
|
||||
size_hint:1,1
|
||||
MDBoxLayout:
|
||||
id:main_container
|
||||
size_hint_y:None
|
||||
padding:"10dp"
|
||||
spacing:"10dp"
|
||||
height: self.minimum_height
|
||||
orientation:"vertical"
|
||||
AnimeHeader:
|
||||
id:header
|
||||
MDBoxLayout:
|
||||
size_hint_y:None
|
||||
height: self.minimum_height
|
||||
AnimeSideBar:
|
||||
id:side_bar
|
||||
screen:root
|
||||
size_hint_y:None
|
||||
height:max(self.parent.height,self.minimum_height)
|
||||
|
||||
MDBoxLayout:
|
||||
spacing:"10dp"
|
||||
orientation:"vertical"
|
||||
size_hint_y:None
|
||||
height: max(self.parent.height,self.minimum_height)
|
||||
RankingsBar:
|
||||
id:rankings_bar
|
||||
Controls:
|
||||
screen:root
|
||||
cols:3 if root.width < 1100 else 5
|
||||
MDBoxLayout:
|
||||
adaptive_height:True
|
||||
padding:"20dp"
|
||||
orientation:"vertical"
|
||||
AnimeDescription:
|
||||
id:anime_description
|
||||
AnimeCharacters:
|
||||
id:anime_characters
|
||||
AnimeReviews:
|
||||
id:anime_reviews
|
||||
BoxLayout:
|
||||
@@ -1,135 +0,0 @@
|
||||
|
||||
from kivy.properties import ObjectProperty, DictProperty, StringProperty
|
||||
|
||||
from anixstream.Utility import anilist_data_helper
|
||||
from anixstream.libs.anilist import AnilistBaseMediaDataSchema
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
from .components import (
|
||||
AnimeHeader,
|
||||
AnimeSideBar,
|
||||
AnimeDescription,
|
||||
AnimeReviews,
|
||||
AnimeCharacters,
|
||||
AnimdlStreamDialog,
|
||||
DownloadAnimeDialog,
|
||||
RankingsBar,
|
||||
)
|
||||
|
||||
|
||||
class AnimeScreenView(BaseScreenView):
|
||||
"""The anime screen view"""
|
||||
caller_screen_name = StringProperty()
|
||||
header: AnimeHeader = ObjectProperty()
|
||||
side_bar: AnimeSideBar = ObjectProperty()
|
||||
rankings_bar: RankingsBar = ObjectProperty()
|
||||
anime_description: AnimeDescription = ObjectProperty()
|
||||
anime_characters: AnimeCharacters = ObjectProperty()
|
||||
anime_reviews: AnimeReviews = ObjectProperty()
|
||||
data = DictProperty()
|
||||
anime_id = 0
|
||||
|
||||
def update_layout(self, data: AnilistBaseMediaDataSchema, caller_screen_name: str):
|
||||
self.caller_screen_name = caller_screen_name
|
||||
self.data = data
|
||||
# uitlity functions
|
||||
|
||||
# variables
|
||||
english_title = data["title"]["english"]
|
||||
jp_title = data["title"]["romaji"]
|
||||
studios = data["studios"]["nodes"]
|
||||
|
||||
# update header
|
||||
self.header.titles = f"{english_title}\n{jp_title}"
|
||||
if banner_image := data["bannerImage"]:
|
||||
self.header.banner_image = banner_image
|
||||
|
||||
# -----side bar-----
|
||||
|
||||
# update image
|
||||
self.side_bar.image = data["coverImage"]["extraLarge"]
|
||||
|
||||
# update alternative titles
|
||||
alternative_titles = {
|
||||
"synonyms": anilist_data_helper.format_list_data_with_comma(
|
||||
data["synonyms"]
|
||||
), # list
|
||||
"japanese": jp_title,
|
||||
"english": english_title,
|
||||
}
|
||||
self.side_bar.alternative_titles = alternative_titles
|
||||
|
||||
# update information
|
||||
information = {
|
||||
"episodes": data["episodes"],
|
||||
"status": data["status"],
|
||||
"nextAiringEpisode": anilist_data_helper.extract_next_airing_episode(
|
||||
data["nextAiringEpisode"]
|
||||
),
|
||||
"aired": f"{anilist_data_helper.format_anilist_date_object(data['startDate'])} to {anilist_data_helper.format_anilist_date_object(data['endDate'])}",
|
||||
"premiered": f"{data['season']} {data['seasonYear']}",
|
||||
"broadcast": data["format"],
|
||||
"countryOfOrigin": data["countryOfOrigin"],
|
||||
"hashtag": data["hashtag"],
|
||||
"studios": anilist_data_helper.format_list_data_with_comma(
|
||||
[studio["name"] for studio in studios if studio["isAnimationStudio"]]
|
||||
), # { "name": "Sunrise", "isAnimationStudio": true }
|
||||
"producers": anilist_data_helper.format_list_data_with_comma(
|
||||
[
|
||||
studio["name"]
|
||||
for studio in studios
|
||||
if not studio["isAnimationStudio"]
|
||||
]
|
||||
),
|
||||
"source": data["source"],
|
||||
"genres": anilist_data_helper.format_list_data_with_comma(data["genres"]),
|
||||
"duration": data["duration"],
|
||||
}
|
||||
self.side_bar.information = information
|
||||
|
||||
# update statistics
|
||||
statistics = [*[(stat["context"], stat["rank"]) for stat in data["rankings"]]]
|
||||
self.side_bar.statistics = statistics
|
||||
|
||||
# update tags
|
||||
self.side_bar.tags = [(tag["name"], tag["rank"]) for tag in data["tags"]]
|
||||
|
||||
# update external links
|
||||
|
||||
external_links = [
|
||||
("AniList", data["siteUrl"]),
|
||||
*[(site["site"], site["url"]) for site in data["externalLinks"]],
|
||||
]
|
||||
self.side_bar.external_links = external_links
|
||||
|
||||
self.rankings_bar.rankings = {
|
||||
"Popularity": data["popularity"],
|
||||
"Favourites": data["favourites"],
|
||||
"AverageScore": data["averageScore"] if data["averageScore"] else 0,
|
||||
}
|
||||
|
||||
self.anime_description.description = data["description"]
|
||||
|
||||
self.anime_characters.characters = [
|
||||
(character["node"], character["voiceActors"])
|
||||
for character in data["characters"]["edges"]
|
||||
] # list (character,actor)
|
||||
|
||||
self.anime_reviews.reviews = data["reviews"]["nodes"]
|
||||
|
||||
def stream_anime_with_custom_cmds_dialog(self,mpv=False):
|
||||
"""
|
||||
Called when user wants to stream with custom commands
|
||||
"""
|
||||
|
||||
AnimdlStreamDialog(self.data,mpv).open()
|
||||
|
||||
def open_download_anime_dialog(self):
|
||||
"""
|
||||
Opens the download anime dialog
|
||||
"""
|
||||
|
||||
DownloadAnimeDialog(self.data).open()
|
||||
|
||||
def add_to_user_anime_list(self, *args):
|
||||
self.app.add_anime_to_user_anime_list(self.model.anime_id)
|
||||
@@ -1,10 +0,0 @@
|
||||
from .side_bar import AnimeSideBar
|
||||
from .header import AnimeHeader
|
||||
from .rankings_bar import RankingsBar
|
||||
from .controls import Controls
|
||||
from .description import AnimeDescription
|
||||
from .characters import AnimeCharacters
|
||||
from .review import AnimeReviews
|
||||
|
||||
from .animdl_stream_dialog import AnimdlStreamDialog
|
||||
from .download_anime_dialog import DownloadAnimeDialog
|
||||
@@ -1,63 +0,0 @@
|
||||
<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 Anime"
|
||||
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"
|
||||
@@ -1,69 +0,0 @@
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.modalview import ModalView
|
||||
|
||||
from kivymd.uix.behaviors import (
|
||||
StencilBehavior,
|
||||
CommonElevationBehavior,
|
||||
BackgroundColorBehavior,
|
||||
)
|
||||
from kivymd.theming import ThemableBehavior
|
||||
|
||||
|
||||
class AnimdlStreamDialog(
|
||||
ThemableBehavior,
|
||||
StencilBehavior,
|
||||
CommonElevationBehavior,
|
||||
BackgroundColorBehavior,
|
||||
ModalView,
|
||||
):
|
||||
"""The anime streaming dialog"""
|
||||
|
||||
def __init__(self, data, mpv, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.data = data
|
||||
self.mpv = mpv
|
||||
if title := data["title"].get("romaji"):
|
||||
self.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):
|
||||
if self.mpv:
|
||||
streaming_cmds = {}
|
||||
title = self.ids.title_field.text
|
||||
streaming_cmds["title"] = title
|
||||
|
||||
episodes_range = self.ids.range_field.text
|
||||
if episodes_range:
|
||||
streaming_cmds["episodes_range"] = episodes_range
|
||||
|
||||
quality = self.ids.quality_field.text
|
||||
if quality:
|
||||
streaming_cmds["quality"] = quality
|
||||
else:
|
||||
streaming_cmds["quality"] = "best"
|
||||
|
||||
app.watch_on_animdl(streaming_cmds)
|
||||
else:
|
||||
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]
|
||||
|
||||
app.watch_on_animdl(custom_options=cmds)
|
||||
self.dismiss()
|
||||
def stream_anime(self, app):
|
||||
Clock.schedule_once(lambda _: self._stream_anime(app))
|
||||
@@ -1,71 +0,0 @@
|
||||
#:import get_hex_from_color kivy.utils.get_hex_from_color
|
||||
|
||||
<CharactersContainer@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
padding:"10dp"
|
||||
orientation:"vertical"
|
||||
|
||||
<CharacterText@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Body"
|
||||
role: "small"
|
||||
|
||||
<CharacterHeader@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
spacing:"10dp"
|
||||
<CharacterAvatar@FitImage>
|
||||
radius:50
|
||||
size_hint:None,None
|
||||
height:"50dp"
|
||||
width:"50dp"
|
||||
|
||||
<CharacterSecondaryContainer@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
orientation:"vertical"
|
||||
<AnimeCharacter>:
|
||||
spacing:"5dp"
|
||||
adaptive_height:True
|
||||
orientation:"vertical"
|
||||
CharacterHeader:
|
||||
padding:"10dp"
|
||||
CharacterAvatar:
|
||||
source:root.character["image"]
|
||||
CharacterText:
|
||||
text: root.character["name"]
|
||||
pos_hint:{"center_y":.5}
|
||||
|
||||
CharacterSecondaryContainer:
|
||||
spacing:"5dp"
|
||||
MDDivider:
|
||||
CharacterText:
|
||||
text: "Details"
|
||||
MDDivider:
|
||||
CharacterText:
|
||||
text:"[color={}]Gender:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["gender"])
|
||||
CharacterText:
|
||||
text:"[color={}]Date Of Birth:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["dateOfBirth"])
|
||||
CharacterText:
|
||||
text:"[color={}]Age:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["age"])
|
||||
CharacterText:
|
||||
text:"[color={}]Description:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.character["description"])
|
||||
max_lines:5
|
||||
CharacterText:
|
||||
text:"[color={}]Voice Actors:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.voice_actors["name"])
|
||||
MDDivider:
|
||||
|
||||
|
||||
<AnimeCharacters>:
|
||||
adaptive_height:True
|
||||
container:container
|
||||
orientation:"vertical"
|
||||
HeaderLabel:
|
||||
text:"Characters"
|
||||
halign:"left"
|
||||
CharactersContainer:
|
||||
id:container
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import ObjectProperty, ListProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class AnimeCharacter(MDBoxLayout):
|
||||
"""an Anime character data"""
|
||||
|
||||
voice_actors = ObjectProperty({"name": "", "image": ""})
|
||||
character = ObjectProperty(
|
||||
{
|
||||
"name": "",
|
||||
"gender": "",
|
||||
"dateOfBirth": "",
|
||||
"image": "",
|
||||
"age": "",
|
||||
"description": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AnimeCharacters(MDBoxLayout):
|
||||
"""The anime characters card"""
|
||||
|
||||
container = ObjectProperty()
|
||||
characters = ListProperty()
|
||||
|
||||
def update_characters_card(self, instance, characters):
|
||||
format_date = lambda date_: (
|
||||
f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else ""
|
||||
)
|
||||
|
||||
self.container.clear_widgets()
|
||||
for character_ in characters: # character (character,actor)
|
||||
character = character_[0]
|
||||
actors = character_[1]
|
||||
|
||||
anime_character = AnimeCharacter()
|
||||
anime_character.character = {
|
||||
"name": character["name"]["full"],
|
||||
"gender": character["gender"],
|
||||
"dateOfBirth": format_date(character["dateOfBirth"]),
|
||||
"image": character["image"]["medium"],
|
||||
"age": character["age"],
|
||||
"description": character["description"],
|
||||
}
|
||||
anime_character.voice_actors = {
|
||||
"name": ", ".join([actor["name"]["full"] for actor in actors])
|
||||
}
|
||||
|
||||
# anime_character.voice_actor =
|
||||
self.container.add_widget(anime_character)
|
||||
|
||||
def on_characters(self, *args):
|
||||
Clock.schedule_once(lambda _: self.update_characters_card(*args))
|
||||
@@ -1,32 +0,0 @@
|
||||
<Controls>
|
||||
adaptive_height:True
|
||||
padding:"10dp"
|
||||
spacing:"10dp"
|
||||
pos_hint: {'center_x': 0.5}
|
||||
cols:3
|
||||
MDButton:
|
||||
on_press:
|
||||
root.screen.add_to_user_anime_list()
|
||||
add_to_user_list_label.text = "Added to MyAnimeList"
|
||||
MDButtonText:
|
||||
id:add_to_user_list_label
|
||||
text:"Add to MyAnimeList"
|
||||
MDButton:
|
||||
on_press:
|
||||
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog()
|
||||
MDButtonText:
|
||||
text:"Watch on Animdl"
|
||||
MDButton:
|
||||
on_press:
|
||||
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog(mpv=True)
|
||||
MDButtonText:
|
||||
text:"Watch on mpv"
|
||||
MDButton:
|
||||
on_press: app.watch_on_allanime(root.screen.data["title"]["romaji"]) if root.screen.data["title"]["romaji"] else app.watch_on_allanime(root.screen.data["title"]["english"])
|
||||
MDButtonText:
|
||||
text:"Watch on AllAnime"
|
||||
MDButton:
|
||||
on_press:
|
||||
if root.screen:root.screen.open_download_anime_dialog()
|
||||
MDButtonText:
|
||||
text:"Download Anime"
|
||||
@@ -1,9 +0,0 @@
|
||||
from kivy.properties import ObjectProperty
|
||||
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
|
||||
|
||||
class Controls(MDGridLayout):
|
||||
"""The diferent controls available"""
|
||||
|
||||
screen = ObjectProperty()
|
||||
@@ -1,23 +0,0 @@
|
||||
<DescriptionContainer@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
padding:"10dp"
|
||||
|
||||
<DescriptionText@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Body"
|
||||
role: "small"
|
||||
<AnimeDescription>:
|
||||
orientation:"vertical"
|
||||
adaptive_height:True
|
||||
HeaderLabel:
|
||||
halign:"left"
|
||||
text:"Description"
|
||||
DescriptionContainer:
|
||||
DescriptionText:
|
||||
text:root.description
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class AnimeDescription(MDBoxLayout):
|
||||
"""The anime description"""
|
||||
|
||||
description = StringProperty()
|
||||
@@ -1,58 +0,0 @@
|
||||
<DownloadDialogLabel@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Label"
|
||||
role: "medium"
|
||||
bold:True
|
||||
<DownloadDialogHeaderLabel@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"
|
||||
|
||||
|
||||
|
||||
<DownloadAnimeDialog>
|
||||
md_bg_color:self.theme_cls.backgroundColor
|
||||
radius:8
|
||||
size_hint:None,None
|
||||
height:"500dp"
|
||||
width:"400dp"
|
||||
MDBoxLayout:
|
||||
spacing: '10dp'
|
||||
padding:"10dp"
|
||||
orientation:"vertical"
|
||||
DownloadDialogHeaderLabel:
|
||||
text:"Download Anime"
|
||||
DownloadDialogLabel:
|
||||
text:"Title"
|
||||
MDTextField:
|
||||
id:title_field
|
||||
required:True
|
||||
DownloadDialogLabel:
|
||||
text:"Range"
|
||||
MDTextField:
|
||||
id:range_field
|
||||
required:True
|
||||
DownloadDialogLabel:
|
||||
text:"Quality"
|
||||
MDTextField:
|
||||
id:quality_field
|
||||
required:True
|
||||
MDBoxLayout:
|
||||
orientation:"vertical"
|
||||
MDButton:
|
||||
pos_hint: {'center_x': 0.5}
|
||||
on_press:root.download_anime(app)
|
||||
MDButtonIcon:
|
||||
icon:"download"
|
||||
MDButtonText:
|
||||
text:"Download"
|
||||
@@ -1,42 +0,0 @@
|
||||
from kivy.uix.modalview import ModalView
|
||||
|
||||
from kivymd.uix.behaviors import (
|
||||
StencilBehavior,
|
||||
CommonElevationBehavior,
|
||||
BackgroundColorBehavior,
|
||||
)
|
||||
from kivymd.theming import ThemableBehavior
|
||||
|
||||
|
||||
# from main import AniXStreamApp
|
||||
class DownloadAnimeDialog(
|
||||
ThemableBehavior,
|
||||
StencilBehavior,
|
||||
CommonElevationBehavior,
|
||||
BackgroundColorBehavior,
|
||||
ModalView,
|
||||
):
|
||||
"""The download anime dialog"""
|
||||
|
||||
def __init__(self, data, **kwargs):
|
||||
super(DownloadAnimeDialog, self).__init__(**kwargs)
|
||||
self.data = data
|
||||
self.anime_id = self.data["id"]
|
||||
if title := data["title"].get("romaji"):
|
||||
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 download_anime(self, app):
|
||||
default_cmds = {}
|
||||
title = self.ids.title_field.text
|
||||
default_cmds["title"] = title
|
||||
if episodes_range := self.ids.range_field.text:
|
||||
default_cmds["episodes_range"] = episodes_range
|
||||
|
||||
if quality := self.ids.range_field.text:
|
||||
default_cmds["quality"] = quality
|
||||
|
||||
app.download_anime(self.anime_id, default_cmds)
|
||||
self.dismiss()
|
||||
@@ -1,20 +0,0 @@
|
||||
<AnimeHeader>:
|
||||
adaptive_height:True
|
||||
orientation: 'vertical'
|
||||
MDBoxLayout:
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.secondaryContainerColor
|
||||
MDLabel:
|
||||
text: root.titles
|
||||
adaptive_height:True
|
||||
padding:"5dp"
|
||||
bold:True
|
||||
shorten:False
|
||||
max_lines:2
|
||||
font_style:"Label"
|
||||
role:"large"
|
||||
FitImage:
|
||||
size_hint_y: None
|
||||
height: dp(250)
|
||||
source: root.banner_image if root.banner_image else app.default_banner_image
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class AnimeHeader(MDBoxLayout):
|
||||
titles = StringProperty()
|
||||
banner_image = StringProperty()
|
||||
@@ -1,81 +0,0 @@
|
||||
#:set yellow [.9,.9,0,.9]
|
||||
|
||||
<RankingsLabel@MDLabel>:
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Label"
|
||||
role: "medium"
|
||||
|
||||
<RankingsHeaderLabel@MDLabel>:
|
||||
color:self.theme_cls.primaryColor
|
||||
bold:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
font_style: "Label"
|
||||
role: "large"
|
||||
|
||||
|
||||
<RankingsDivider@MDDivider>:
|
||||
orientation:"vertical"
|
||||
|
||||
<RankingsBoxLayout@MDBoxLayout>:
|
||||
orientation:"vertical"
|
||||
padding:"20dp"
|
||||
<RankingsBar>:
|
||||
size_hint_y:None
|
||||
height:dp(100)
|
||||
line_color:self.theme_cls.secondaryColor
|
||||
padding:"10dp"
|
||||
RankingsBoxLayout:
|
||||
size_hint_x:.4
|
||||
RankingsHeaderLabel:
|
||||
text:"Average Score"
|
||||
MDBoxLayout:
|
||||
adaptive_width:True
|
||||
MDBoxLayout:
|
||||
adaptive_size:True
|
||||
pos_hint: {'center_y': .5}
|
||||
MDIcon:
|
||||
icon: "star"
|
||||
color:yellow
|
||||
disabled: not((root.rankings["AverageScore"]/100)*6>=1)
|
||||
MDIcon:
|
||||
color:yellow
|
||||
disabled: not(root.rankings["AverageScore"]/100*6>=2)
|
||||
icon: "star"
|
||||
MDIcon:
|
||||
color:yellow
|
||||
disabled: not(root.rankings["AverageScore"]/100*6>=3)
|
||||
icon: "star"
|
||||
MDIcon:
|
||||
color:yellow
|
||||
disabled: not(root.rankings["AverageScore"]/100*6>=4)
|
||||
icon: "star"
|
||||
MDIcon:
|
||||
color:yellow
|
||||
icon: "star"
|
||||
disabled: not(root.rankings["AverageScore"]/100*6>=5)
|
||||
MDIcon:
|
||||
color:yellow
|
||||
icon: "star"
|
||||
disabled: not(root.rankings["AverageScore"]/100*6>=6)
|
||||
RankingsLabel:
|
||||
adaptive_width:True
|
||||
text: '{}'.format(root.rankings["AverageScore"]/10)
|
||||
RankingsDivider:
|
||||
RankingsBoxLayout:
|
||||
size_hint_x:.3
|
||||
RankingsHeaderLabel:
|
||||
text:"Popularity"
|
||||
RankingsLabel:
|
||||
text: '{}'.format(root.rankings["Popularity"])
|
||||
RankingsDivider:
|
||||
RankingsBoxLayout:
|
||||
size_hint_x:.3
|
||||
RankingsHeaderLabel:
|
||||
text:"Favourites"
|
||||
RankingsLabel:
|
||||
text: '{}'.format(root.rankings["Favourites"])
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from kivy.properties import DictProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class RankingsBar(MDBoxLayout):
|
||||
rankings = DictProperty(
|
||||
{
|
||||
"Popularity": 0,
|
||||
"Favourites": 0,
|
||||
"AverageScore": 0,
|
||||
}
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
#:import get_hex_from_color kivy.utils.get_hex_from_color
|
||||
|
||||
|
||||
<ReviewContainer@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
padding:"10dp"
|
||||
orientation:"vertical"
|
||||
|
||||
<ReviewText@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Body"
|
||||
role: "small"
|
||||
|
||||
<ReviewHeader@MDBoxLayout>:
|
||||
adaptive_height:True
|
||||
spacing:"10dp"
|
||||
padding:"10dp"
|
||||
|
||||
<ReviewerAvatar@FitImage>
|
||||
radius:50
|
||||
size_hint:None,None
|
||||
height:"50dp"
|
||||
width:"50dp"
|
||||
|
||||
<AnimeReview>
|
||||
orientation:"vertical"
|
||||
adaptive_height:True
|
||||
ReviewHeader:
|
||||
ReviewerAvatar:
|
||||
source:root.review["avatar"]
|
||||
ReviewText:
|
||||
pos_hint: {'center_y': 0.5}
|
||||
text:root.review["username"]
|
||||
MDDivider:
|
||||
ReviewText:
|
||||
text:root.review["summary"]
|
||||
MDDivider:
|
||||
<AnimeReviews>:
|
||||
container:container
|
||||
adaptive_height:True
|
||||
orientation:"vertical"
|
||||
HeaderLabel:
|
||||
halign:"left"
|
||||
text:"reviews"
|
||||
ReviewContainer:
|
||||
id:container
|
||||
@@ -1,29 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, ListProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class AnimeReview(MDBoxLayout):
|
||||
review = ObjectProperty({"username": "", "avatar": "", "summary": ""})
|
||||
|
||||
|
||||
class AnimeReviews(MDBoxLayout):
|
||||
"""anime reviews"""
|
||||
|
||||
reviews = ListProperty()
|
||||
container = ObjectProperty()
|
||||
|
||||
def on_reviews(self, *args):
|
||||
Clock.schedule_once(lambda _: self.update_reviews_card(*args))
|
||||
|
||||
def update_reviews_card(self, instance, reviews):
|
||||
self.container.clear_widgets()
|
||||
for review in reviews:
|
||||
review_ = AnimeReview()
|
||||
review_.review = {
|
||||
"username": review["user"]["name"],
|
||||
"avatar": review["user"]["avatar"]["medium"],
|
||||
"summary": review["summary"],
|
||||
}
|
||||
self.container.add_widget(review_)
|
||||
@@ -1,102 +0,0 @@
|
||||
#:import get_hex_from_color kivy.utils.get_hex_from_color
|
||||
|
||||
<FitBoxLayout@MDBoxLayout>:
|
||||
size_hint_y:None
|
||||
height:self.minimum_height
|
||||
padding:"10dp"
|
||||
spacing:"10dp"
|
||||
orientation: 'vertical'
|
||||
pos_hint: {'center_x': 0.5}
|
||||
<SideBarLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Label"
|
||||
role: "medium"
|
||||
<HeaderLabel>:
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.secondaryContainerColor
|
||||
MDLabel:
|
||||
text:root.text
|
||||
adaptive_height:True
|
||||
halign:root.halign
|
||||
max_lines:0
|
||||
shorten:False
|
||||
bold:True
|
||||
font_style: "Label"
|
||||
role: "large"
|
||||
padding:"10dp"
|
||||
|
||||
<AnimeSideBar>:
|
||||
size_hint_x: None
|
||||
width: dp(300)
|
||||
orientation: 'vertical'
|
||||
line_color:self.theme_cls.secondaryColor
|
||||
statistics_container:statistics_container
|
||||
tags_container:tags_container
|
||||
external_links_container:external_links_container
|
||||
FitBoxLayout:
|
||||
FitImage:
|
||||
source:root.image
|
||||
size_hint:None,None
|
||||
height:dp(250)
|
||||
width:dp(200)
|
||||
pos_hint: {'center_x': 0.5}
|
||||
MDButton:
|
||||
pos_hint: {'center_x': 0.5}
|
||||
on_press:
|
||||
if root.screen:root.screen.stream_anime_with_custom_cmds_dialog(mpv=True)
|
||||
MDButtonText:
|
||||
text:"Watch with mpv"
|
||||
FitBoxLayout:
|
||||
HeaderLabel:
|
||||
text:"Alternative Titles"
|
||||
SideBarLabel:
|
||||
text: "[color={}]Synonyms:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["synonyms"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]English:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["english"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Japanese:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.alternative_titles["japanese"])
|
||||
FitBoxLayout:
|
||||
HeaderLabel:
|
||||
text:"Information"
|
||||
SideBarLabel:
|
||||
text: "[color={}]Episodes:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["episodes"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Status:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["status"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Next Airing Episode:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["nextAiringEpisode"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Aired:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["aired"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Premiered:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["premiered"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Broadcast:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["broadcast"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Country Of Origin:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["countryOfOrigin"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Hashtag:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["hashtag"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Studios:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["studios"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Producers:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["producers"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Source:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["source"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Genres:[/color] {}".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["genres"])
|
||||
SideBarLabel:
|
||||
text: "[color={}]Duration:[/color] {} minutes".format(get_hex_from_color(self.theme_cls.primaryColor),root.information["duration"])
|
||||
FitBoxLayout:
|
||||
id:statistics_container
|
||||
HeaderLabel:
|
||||
text:"Rankings"
|
||||
FitBoxLayout:
|
||||
id:tags_container
|
||||
HeaderLabel:
|
||||
text:"Tags"
|
||||
FitBoxLayout:
|
||||
id:external_links_container
|
||||
HeaderLabel:
|
||||
text:"External Links"
|
||||
BoxLayout:
|
||||
@@ -1,98 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, StringProperty, DictProperty, ListProperty
|
||||
from kivy.utils import get_hex_from_color
|
||||
from kivy.factory import Factory
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.label import MDLabel
|
||||
|
||||
|
||||
class HeaderLabel(MDBoxLayout):
|
||||
text = StringProperty()
|
||||
halign = StringProperty("center")
|
||||
|
||||
|
||||
Factory.register("HeaderLabel", HeaderLabel)
|
||||
|
||||
|
||||
class SideBarLabel(MDLabel):
|
||||
pass
|
||||
|
||||
|
||||
# TODO:Switch to using the kivy_markup_module
|
||||
class AnimeSideBar(MDBoxLayout):
|
||||
screen = ObjectProperty()
|
||||
image = StringProperty()
|
||||
alternative_titles = DictProperty(
|
||||
{
|
||||
"synonyms": "",
|
||||
"english": "",
|
||||
"japanese": "",
|
||||
}
|
||||
)
|
||||
information = DictProperty(
|
||||
{
|
||||
"episodes": "",
|
||||
"status": "",
|
||||
"aired": "",
|
||||
"nextAiringEpisode": "",
|
||||
"premiered": "",
|
||||
"broadcast": "",
|
||||
"countryOfOrigin": "",
|
||||
"hashtag": "",
|
||||
"studios": "", # { "name": "Sunrise", "isAnimationStudio": true }
|
||||
"source": "",
|
||||
"genres": "",
|
||||
"duration": "",
|
||||
"producers": "",
|
||||
}
|
||||
)
|
||||
statistics = ListProperty()
|
||||
statistics_container = ObjectProperty()
|
||||
external_links = ListProperty()
|
||||
external_links_container = ObjectProperty()
|
||||
tags = ListProperty()
|
||||
tags_container = ObjectProperty()
|
||||
|
||||
def on_statistics(self, instance, value):
|
||||
self.statistics_container.clear_widgets()
|
||||
header = HeaderLabel()
|
||||
header.text = "Rankings"
|
||||
self.statistics_container.add_widget(header)
|
||||
for stat in value:
|
||||
# stat (rank,context)
|
||||
label = SideBarLabel()
|
||||
label.text = "[color={}]{}:[/color] {}".format(
|
||||
get_hex_from_color(label.theme_cls.primaryColor),
|
||||
stat[0].capitalize(),
|
||||
f"{stat[1]}",
|
||||
)
|
||||
self.statistics_container.add_widget(label)
|
||||
|
||||
def on_tags(self, instance, value):
|
||||
self.tags_container.clear_widgets()
|
||||
header = HeaderLabel()
|
||||
header.text = "Tags"
|
||||
self.tags_container.add_widget(header)
|
||||
for tag in value:
|
||||
label = SideBarLabel()
|
||||
label.text = "[color={}]{}:[/color] {}".format(
|
||||
get_hex_from_color(label.theme_cls.primaryColor),
|
||||
tag[0].capitalize(),
|
||||
f"{tag[1]} %",
|
||||
)
|
||||
self.tags_container.add_widget(label)
|
||||
|
||||
def on_external_links(self, instance, value):
|
||||
self.external_links_container.clear_widgets()
|
||||
header = HeaderLabel()
|
||||
header.text = "External Links"
|
||||
self.external_links_container.add_widget(header)
|
||||
for site in value:
|
||||
# stat (rank,context)
|
||||
label = SideBarLabel()
|
||||
label.text = "[color={}]{}:[/color] {}".format(
|
||||
get_hex_from_color(label.theme_cls.primaryColor),
|
||||
site[0].capitalize(),
|
||||
site[1],
|
||||
)
|
||||
self.external_links_container.add_widget(label)
|
||||
@@ -1,21 +0,0 @@
|
||||
#: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:
|
||||
MDScrollView:
|
||||
MDLabel:
|
||||
text:root.crash_text
|
||||
adaptive_height:True
|
||||
markup:True
|
||||
padding:"10dp"
|
||||
@@ -1,21 +0,0 @@
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
from anixstream.Utility.utils import read_crash_file
|
||||
from anixstream.Utility.kivy_markup_helper import color_text, bolden
|
||||
|
||||
|
||||
class CrashLogScreenView(BaseScreenView):
|
||||
"""The crash log screen"""
|
||||
|
||||
crash_text = StringProperty()
|
||||
|
||||
def __init__(self, **kw):
|
||||
super().__init__(**kw)
|
||||
if crashes := read_crash_file():
|
||||
self.crash_text = crashes
|
||||
else:
|
||||
self.crash_text = color_text(
|
||||
f"No Crashes so far :) and if there are any in the future {bolden('please report! Okay?')}",
|
||||
self.theme_cls.primaryColor,
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
#:import color_text anixstream.Utility.kivy_markup_helper.color_text
|
||||
|
||||
<TaskText@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Label"
|
||||
role: "large"
|
||||
bold:True
|
||||
|
||||
|
||||
<TaskCard>:
|
||||
adaptive_height:True
|
||||
radius:8
|
||||
padding:"20dp"
|
||||
md_bg_color:self.theme_cls.surfaceContainerHighColor
|
||||
|
||||
TaskText:
|
||||
size_hint_x:.8
|
||||
text:color_text(root.anime_task_name,root.theme_cls.primaryColor)
|
||||
TaskText:
|
||||
size_hint_x:.2
|
||||
# color:self.theme_cls.surfaceDimColor
|
||||
theme_text_color:"Secondary"
|
||||
text:color_text(root.episodes_to_download,root.theme_cls.secondaryColor)
|
||||
MDIcon:
|
||||
icon:"download"
|
||||
@@ -1,12 +0,0 @@
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
# TODO: add a progress bar to show the individual progress of each task
|
||||
class TaskCard(MDBoxLayout):
|
||||
anime_task_name = StringProperty()
|
||||
episodes_to_download = StringProperty()
|
||||
def __init__(self, anime_title:str, episodes:str, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.anime_task_name = f"{anime_title}"
|
||||
self.episodes_to_download = f"Episodes: {episodes}"
|
||||
@@ -1,58 +0,0 @@
|
||||
#:import get_color_from_hex kivy.utils.get_color_from_hex
|
||||
#:import StringProperty kivy.properties.StringProperty
|
||||
|
||||
<DownloadsScreenLabel@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Label"
|
||||
role: "large"
|
||||
bold:True
|
||||
<DownloadsScreenView>
|
||||
md_bg_color: self.theme_cls.backgroundColor
|
||||
main_container:main_container
|
||||
download_progress_label:download_progress_label
|
||||
progress_bar:progress_bar
|
||||
MDBoxLayout:
|
||||
NavRail:
|
||||
screen:root
|
||||
MDAnchorLayout:
|
||||
anchor_y: 'top'
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
SearchBar:
|
||||
MDScrollView:
|
||||
size_hint:.95,1
|
||||
MDBoxLayout:
|
||||
id:main_container
|
||||
orientation:"vertical"
|
||||
padding:"40dp"
|
||||
pos_hint:{"center_x":.5}
|
||||
spacing:"10dp"
|
||||
adaptive_height:True
|
||||
HeaderLabel:
|
||||
text:"Download Tasks"
|
||||
halign:"left"
|
||||
MDIcon:
|
||||
padding:"10dp"
|
||||
pos_hint:{"center_y":.5}
|
||||
icon:"clock"
|
||||
MDBoxLayout:
|
||||
size_hint_y:None
|
||||
height:"40dp"
|
||||
spacing:"10dp"
|
||||
padding:"10dp"
|
||||
md_bg_color:self.theme_cls.secondaryContainerColor
|
||||
DownloadsScreenLabel:
|
||||
id:download_progress_label
|
||||
size_hint_x: .6
|
||||
text:"Try Downloading sth :)"
|
||||
pos_hint: {'center_y': .5}
|
||||
MDLinearProgressIndicator:
|
||||
id: progress_bar
|
||||
size_hint_x: .4
|
||||
size_hint_y:None
|
||||
height:"10dp"
|
||||
type: "determinate"
|
||||
pos_hint: {'center_y': .5}
|
||||
@@ -1,32 +0,0 @@
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.logger import Logger
|
||||
from kivy.utils import format_bytes_to_human
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
from .components.task_card import TaskCard
|
||||
|
||||
|
||||
class DownloadsScreenView(BaseScreenView):
|
||||
main_container = ObjectProperty()
|
||||
progress_bar = ObjectProperty()
|
||||
download_progress_label = ObjectProperty()
|
||||
|
||||
def on_new_download_task(self, anime_title: str, episodes: str | None):
|
||||
if not episodes:
|
||||
episodes = "All"
|
||||
Clock.schedule_once(
|
||||
lambda _: self.main_container.add_widget(TaskCard(anime_title, episodes))
|
||||
)
|
||||
|
||||
def on_episode_download_progress(
|
||||
self, current_bytes_downloaded, total_bytes, episode_info
|
||||
):
|
||||
percentage_completion = round((current_bytes_downloaded / total_bytes) * 100)
|
||||
progress_text = f"Downloading: {episode_info['anime_title']} - {episode_info['episode']} ({format_bytes_to_human(current_bytes_downloaded)}/{format_bytes_to_human(total_bytes)})"
|
||||
if (percentage_completion % 5) == 0:
|
||||
self.progress_bar.value = max(min(percentage_completion, 100), 0)
|
||||
self.download_progress_label.text = progress_text
|
||||
|
||||
def update_layout(self, widget):
|
||||
self.user_anime_list_container.add_widget(widget)
|
||||
@@ -1,60 +0,0 @@
|
||||
#:import get_color_from_hex kivy.utils.get_color_from_hex
|
||||
#:import StringProperty kivy.properties.StringProperty
|
||||
|
||||
<HelpCard@MDBoxLayout>
|
||||
spacing:"10dp"
|
||||
orientation:"vertical"
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
theme_text_color:"Secondary"
|
||||
|
||||
<HelpHeaderLabel@HeaderLabel>
|
||||
halign:"left"
|
||||
|
||||
<HelpDescription@MDLabel>:
|
||||
adaptive_height:True
|
||||
max_lines:0
|
||||
shorten:False
|
||||
markup:True
|
||||
font_style: "Body"
|
||||
padding:"10dp"
|
||||
role: "large"
|
||||
|
||||
<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:
|
||||
MDScrollView:
|
||||
size_hint_x:.95
|
||||
MDBoxLayout:
|
||||
adaptive_height:True
|
||||
padding:"10dp"
|
||||
orientation:"vertical"
|
||||
HelpCard:
|
||||
HelpHeaderLabel:
|
||||
text:"Animdl Commands"
|
||||
HelpDescription:
|
||||
text:root.animdl_help
|
||||
HelpCard:
|
||||
HelpHeaderLabel:
|
||||
text:"Installing Animdl"
|
||||
HelpDescription:
|
||||
text:root.installing_animdl_help
|
||||
HelpCard:
|
||||
HelpHeaderLabel:
|
||||
text:"Available Themes"
|
||||
HelpDescription:
|
||||
text:root.available_themes
|
||||
HelpCard:
|
||||
HelpHeaderLabel:
|
||||
text:"About"
|
||||
HelpDescription:
|
||||
text:"This app was made to be a gui wrapper for any and all anime cli tools. Inoder to solve the age old problem of getting the same experience from the cli as you would in a website"
|
||||
@@ -1,72 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, StringProperty
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
from anixstream.Utility.kivy_markup_helper import bolden, color_text, underline
|
||||
from anixstream.Utility.data import themes_available
|
||||
|
||||
|
||||
class HelpScreenView(BaseScreenView):
|
||||
main_container = ObjectProperty()
|
||||
animdl_help = StringProperty()
|
||||
installing_animdl_help = StringProperty()
|
||||
available_themes = StringProperty()
|
||||
|
||||
def __init__(self, **kw):
|
||||
super(HelpScreenView, self).__init__(**kw)
|
||||
self.animdl_help = f"""
|
||||
{underline(color_text(bolden('Streaming Commands'),self.theme_cls.surfaceBrightColor))}
|
||||
{color_text(bolden('-r:'),self.theme_cls.primaryFixedDimColor)} specifies the range of episodes
|
||||
example: {color_text(('animdl stream <Anime title> -r 1-4'),self.theme_cls.tertiaryColor)}
|
||||
explanation:in this case gets 4 episodes 1 to 4
|
||||
{color_text(('-s:'),self.theme_cls.primaryFixedDimColor)} special selector for the most recent episodes or basically selects from the end
|
||||
example: {color_text(('animdl stream <Anime title> -s 4'),self.theme_cls.tertiaryColor)}
|
||||
explanation: in this case gets the latest 4 episodes
|
||||
{color_text(('-q:'),self.theme_cls.primaryFixedDimColor)} sets the quality of the stream
|
||||
example: {color_text(('animdl stream <Anime title> -q best'),self.theme_cls.tertiaryColor)}
|
||||
explanation: The quality of the anime stream should be the best possible others include 1080,720... plus worst
|
||||
{underline(color_text(bolden('Downloading Commands'),self.theme_cls.surfaceBrightColor))}
|
||||
{color_text(bolden('-r:'),self.theme_cls.primaryFixedDimColor)} specifies the range of episodes
|
||||
example: {color_text(('animdl download <Anime title> -r 1-4'),self.theme_cls.tertiaryColor)}
|
||||
explanation:in this case gets 4 episodes 1 to 4
|
||||
{color_text(('-s:'),self.theme_cls.primaryFixedDimColor)} special selector for the most recent episodes or basically selects from the end
|
||||
example: {color_text(('animdl download <Anime title> -s 4'),self.theme_cls.tertiaryColor)}
|
||||
explanation: in this case gets the latest 4 episodes
|
||||
{color_text(('-q:'),self.theme_cls.primaryFixedDimColor)} sets the quality of the download
|
||||
example: {color_text(('animdl download <Anime title> -q best'),self.theme_cls.tertiaryColor)}
|
||||
explanation: The quality of the anime download should be the best possible others include 1080,720... plus worst
|
||||
"""
|
||||
self.installing_animdl_help = f"""
|
||||
This works on windows only and should be done in powershell
|
||||
1. First install pyenv with the following command:
|
||||
{color_text(('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"'),self.theme_cls.tertiaryColor)}
|
||||
2. run the following command to check if successsful:
|
||||
{color_text(('pyenv --version '),self.theme_cls.tertiaryColor)}
|
||||
3. run the following command to install python 3.10
|
||||
{color_text(('pyenv install 3.10'),self.theme_cls.tertiaryColor)}
|
||||
4. To confirm successful install of python 3.10 run the following command and check if 3.10 is listed:
|
||||
{color_text(('pyenv -l'),self.theme_cls.tertiaryColor)}
|
||||
5. Next run:
|
||||
{color_text(('pyenv local 3.10'),self.theme_cls.tertiaryColor)} (if in anixstream directory to set python 3.10 as local interpreter)
|
||||
or run:
|
||||
{color_text(('pyenv global 3.10'),self.theme_cls.tertiaryColor)} (if in another directory to set python version 3.10 as global interpreter)
|
||||
6. Check if success by running and checking if output is 3.10:
|
||||
{color_text(('python --version'),self.theme_cls.tertiaryColor)}
|
||||
7. Run:
|
||||
{color_text(('python -m pip install animdl'),self.theme_cls.tertiaryColor)}
|
||||
8. Check if success by running:
|
||||
{color_text(('python -m animdl'),self.theme_cls.tertiaryColor)}
|
||||
{color_text(('Note:'),self.theme_cls.secondaryColor)}
|
||||
All this instructions should be done from the folder you choose to install
|
||||
aniXstream but incase you have never installed python should work any where
|
||||
{bolden('-----------------------------')}
|
||||
Now enjoy :)
|
||||
{bolden('-----------------------------')}
|
||||
"""
|
||||
self.available_themes = "\n".join(themes_available)
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -1,23 +0,0 @@
|
||||
<HomeScreenView>
|
||||
md_bg_color: self.theme_cls.backgroundColor
|
||||
main_container:main_container
|
||||
MDBoxLayout:
|
||||
NavRail:
|
||||
screen:root
|
||||
MDAnchorLayout:
|
||||
anchor_y: 'top'
|
||||
padding:"10dp"
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
id:p
|
||||
SearchBar:
|
||||
MDScrollView:
|
||||
size_hint:1,1
|
||||
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)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from kivy.properties import ObjectProperty
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
|
||||
|
||||
class HomeScreenView(BaseScreenView):
|
||||
main_container = ObjectProperty()
|
||||
@@ -1,28 +0,0 @@
|
||||
<MyListScreenView>
|
||||
md_bg_color: self.theme_cls.backgroundColor
|
||||
user_anime_list_container:user_anime_list_container
|
||||
MDBoxLayout:
|
||||
size_hint:1,1
|
||||
NavRail:
|
||||
screen:root
|
||||
MDAnchorLayout:
|
||||
anchor_y: 'top'
|
||||
padding:"10dp"
|
||||
size_hint:1,1
|
||||
MDBoxLayout:
|
||||
spacing:"40dp"
|
||||
orientation: 'vertical'
|
||||
size_hint:.95,1
|
||||
SearchBar:
|
||||
MDScrollView:
|
||||
pos_hint:{"center_x":.5}
|
||||
size_hint:1,1
|
||||
MDGridLayout:
|
||||
spacing: '40dp'
|
||||
padding: "100dp","50dp","10dp","200dp"
|
||||
id:user_anime_list_container
|
||||
cols:4 if root.width<=1100 else 5
|
||||
size_hint_y:None
|
||||
height:self.minimum_height
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, StringProperty, DictProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
|
||||
|
||||
class MyListScreenView(BaseScreenView):
|
||||
user_anime_list_container = ObjectProperty()
|
||||
|
||||
def model_is_changed(self) -> None:
|
||||
"""
|
||||
Called whenever any change has occurred in the data model.
|
||||
The view in this method tracks these changes and updates the UI
|
||||
according to these changes.
|
||||
"""
|
||||
|
||||
def on_enter(self):
|
||||
Clock.schedule_once(lambda _: self.controller.requested_update_my_list_screen())
|
||||
|
||||
def update_layout(self, widget):
|
||||
self.user_anime_list_container.add_widget(widget)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .filters import Filters
|
||||
from .pagination import SearchResultsPagination
|
||||
from .trending_sidebar import TrendingAnimeSideBar
|
||||
@@ -1,27 +0,0 @@
|
||||
<FilterDropDown>:
|
||||
MDDropDownItemText:
|
||||
text: root.text
|
||||
|
||||
<FilterLabel@MDLabel>:
|
||||
adaptive_width:True
|
||||
|
||||
<Filters>:
|
||||
adaptive_height:True
|
||||
spacing:"10dp"
|
||||
size_hint_x:.95
|
||||
pos_hint:{"center_x":.5}
|
||||
padding:"10dp"
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
|
||||
FilterLabel:
|
||||
text:"Sort By"
|
||||
FilterDropDown:
|
||||
id:sort_filter
|
||||
text:root.filters["sort"]
|
||||
on_release: root.open_filter_menu(self,"sort")
|
||||
FilterLabel:
|
||||
text:"Status"
|
||||
FilterDropDown:
|
||||
id:status_filter
|
||||
text:root.filters["status"]
|
||||
on_release: root.open_filter_menu(self,"status")
|
||||
@@ -1,83 +0,0 @@
|
||||
from kivy.properties import StringProperty, DictProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
|
||||
|
||||
class FilterDropDown(MDDropDownItem):
|
||||
text: str = StringProperty()
|
||||
|
||||
|
||||
class Filters(MDBoxLayout):
|
||||
filters: dict = DictProperty({"sort": "SEARCH_MATCH", "status": "FINISHED"})
|
||||
|
||||
def open_filter_menu(self, menu_item, filter_name):
|
||||
items = []
|
||||
match filter_name:
|
||||
case "sort":
|
||||
items = [
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMANJI",
|
||||
"TITLE_ROMANJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
case "status":
|
||||
items = [
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT_YET_RELEASED",
|
||||
"CANCELLED",
|
||||
"HIATUS",
|
||||
]
|
||||
case _:
|
||||
items = []
|
||||
if items:
|
||||
menu_items = [
|
||||
{
|
||||
"text": f"{item}",
|
||||
"on_release": lambda filter_value=f"{item}": self.filter_menu_callback(
|
||||
filter_name, filter_value
|
||||
),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
MDDropdownMenu(caller=menu_item, items=menu_items).open()
|
||||
|
||||
def filter_menu_callback(self, filter_name, filter_value):
|
||||
match filter_name:
|
||||
case "sort":
|
||||
self.ids.sort_filter.text = filter_value
|
||||
self.filters["sort"] = filter_value
|
||||
case "status":
|
||||
self.ids.status_filter.text = filter_value
|
||||
self.filters["status"] = filter_value
|
||||
@@ -1,21 +0,0 @@
|
||||
<PaginationLabel@MDLabel>:
|
||||
max_lines:0
|
||||
shorten:False
|
||||
adaptive_height:True
|
||||
font_style: "Label"
|
||||
pos_hint:{"center_y":.5}
|
||||
halign:"center"
|
||||
role: "medium"
|
||||
|
||||
<SearchResultsPagination>:
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
radius:8
|
||||
adaptive_height:True
|
||||
MDIconButton:
|
||||
icon:"arrow-left"
|
||||
on_release:root.search_view.previous_page()
|
||||
PaginationLabel:
|
||||
text:"Page {} of {}".format(root.current_page,root.total_pages)
|
||||
MDIconButton:
|
||||
icon:"arrow-right"
|
||||
on_release:root.search_view.next_page()
|
||||
@@ -1,9 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, NumericProperty
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class SearchResultsPagination(MDBoxLayout):
|
||||
current_page = NumericProperty()
|
||||
total_pages = NumericProperty()
|
||||
search_view = ObjectProperty()
|
||||
@@ -1,6 +0,0 @@
|
||||
<TrendingAnimeSideBar>:
|
||||
orientation: 'vertical'
|
||||
adaptive_height:True
|
||||
md_bg_color:self.theme_cls.surfaceContainerLowColor
|
||||
pos_hint: {'center_x': 0.5}
|
||||
padding:"25dp","25dp","25dp","200dp"
|
||||
@@ -1,5 +0,0 @@
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
|
||||
|
||||
class TrendingAnimeSideBar(MDBoxLayout):
|
||||
pass
|
||||
@@ -1,58 +0,0 @@
|
||||
<SearchScreenView>
|
||||
md_bg_color: self.theme_cls.backgroundColor
|
||||
search_results_container:search_results_container
|
||||
trending_anime_sidebar:trending_anime_sidebar
|
||||
search_results_pagination:search_results_pagination
|
||||
filters:filters
|
||||
MDBoxLayout:
|
||||
size_hint:1,1
|
||||
NavRail:
|
||||
screen:root
|
||||
MDAnchorLayout:
|
||||
anchor_y: 'top'
|
||||
padding:"10dp"
|
||||
size_hint:1,1
|
||||
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint:1,1
|
||||
SearchBar:
|
||||
MDBoxLayout:
|
||||
spacing:"20dp"
|
||||
padding:"75dp","10dp","100dp","0dp"
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint:1,1
|
||||
Filters:
|
||||
id:filters
|
||||
MDBoxLayout:
|
||||
spacing:"20dp"
|
||||
MDScrollView:
|
||||
size_hint:1,1
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint_y:None
|
||||
height:max(self.parent.parent.height,self.minimum_height)
|
||||
MDGridLayout:
|
||||
pos_hint: {'center_x': 0.5}
|
||||
id:search_results_container
|
||||
spacing: '40dp'
|
||||
padding: "25dp","50dp","75dp","200dp"
|
||||
cols:3 if root.width <= 1100 else 5
|
||||
size_hint_y:None
|
||||
height:max(self.parent.parent.height,self.minimum_height)
|
||||
SearchResultsPagination:
|
||||
id:search_results_pagination
|
||||
search_view:root
|
||||
MDBoxLayout:
|
||||
orientation:"vertical"
|
||||
size_hint_y:1
|
||||
size_hint_x:None
|
||||
width: dp(250)
|
||||
HeaderLabel:
|
||||
text:"Trending"
|
||||
MDScrollView:
|
||||
TrendingAnimeSideBar:
|
||||
id:trending_anime_sidebar
|
||||
height:max(self.parent.parent.height,self.minimum_height)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, StringProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from anixstream.View.base_screen import BaseScreenView
|
||||
from .components import TrendingAnimeSideBar, Filters, SearchResultsPagination
|
||||
|
||||
|
||||
class SearchScreenView(BaseScreenView):
|
||||
trending_anime_sidebar: TrendingAnimeSideBar = ObjectProperty()
|
||||
search_results_pagination: SearchResultsPagination = ObjectProperty()
|
||||
filters: Filters = ObjectProperty()
|
||||
|
||||
search_results_container = ObjectProperty()
|
||||
search_term: str = StringProperty()
|
||||
is_searching = False
|
||||
has_next_page = False
|
||||
current_page = 0
|
||||
total_pages = 0
|
||||
|
||||
def handle_search_for_anime(self, search_widget=None, page=None):
|
||||
if search_widget:
|
||||
search_term = search_widget.text
|
||||
elif page:
|
||||
search_term = self.search_term
|
||||
else:
|
||||
return
|
||||
|
||||
if search_term and not (self.is_searching):
|
||||
self.search_term = search_term
|
||||
self.search_results_container.clear_widgets()
|
||||
if filters := self.filters.filters:
|
||||
Clock.schedule_once(
|
||||
lambda _: self.controller.requested_search_for_anime(
|
||||
search_term, **filters, page=page
|
||||
)
|
||||
)
|
||||
else:
|
||||
Clock.schedule_once(
|
||||
lambda _: self.controller.requested_search_for_anime(
|
||||
search_term, page=page
|
||||
)
|
||||
)
|
||||
|
||||
def update_layout(self, widget):
|
||||
self.search_results_container.add_widget(widget)
|
||||
|
||||
def update_pagination(self, pagination_info):
|
||||
self.search_results_pagination.current_page = self.current_page = (
|
||||
pagination_info["currentPage"]
|
||||
)
|
||||
self.search_results_pagination.total_pages = self.total_pages = max(
|
||||
int(pagination_info["total"] / 30), 1
|
||||
)
|
||||
self.has_next_page = pagination_info["hasNextPage"]
|
||||
|
||||
def next_page(self):
|
||||
if self.has_next_page:
|
||||
page = self.current_page + 1
|
||||
self.handle_search_for_anime(page=page)
|
||||
|
||||
def previous_page(self):
|
||||
if self.current_page > 1:
|
||||
page = self.current_page - 1
|
||||
self.handle_search_for_anime(page=page)
|
||||
|
||||
def update_trending_sidebar(self, trending_anime):
|
||||
self.trending_anime_sidebar.add_widget(trending_anime)
|
||||
@@ -1,8 +0,0 @@
|
||||
# screens
|
||||
from .HomeScreen.home_screen import HomeScreenView
|
||||
from .SearchScreen.search_screen import SearchScreenView
|
||||
from .MylistScreen.my_list_screen import MyListScreenView
|
||||
from .AnimeScreen.anime_screen import AnimeScreenView
|
||||
from .CrashLogScreen.crashlog_screen import CrashLogScreenView
|
||||
from .DownloadsScreen.download_screen import DownloadsScreenView
|
||||
from .HelpScreen.help_screen import HelpScreenView
|
||||
@@ -1,72 +0,0 @@
|
||||
from kivy.properties import ObjectProperty, StringProperty
|
||||
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.screen import MDScreen
|
||||
from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.button import MDIconButton
|
||||
from kivymd.uix.tooltip import MDTooltip
|
||||
|
||||
from anixstream.Utility.observer import Observer
|
||||
|
||||
|
||||
class NavRail(MDNavigationRail):
|
||||
screen = ObjectProperty()
|
||||
|
||||
|
||||
class SearchBar(MDBoxLayout):
|
||||
screen = ObjectProperty()
|
||||
|
||||
|
||||
class Tooltip(MDTooltip):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipMDIconButton(Tooltip, MDIconButton):
|
||||
tooltip_text = StringProperty()
|
||||
|
||||
|
||||
class CommonNavigationRailItem(MDNavigationRailItem):
|
||||
icon = StringProperty()
|
||||
text = StringProperty()
|
||||
|
||||
|
||||
class BaseScreenView(MDScreen, Observer):
|
||||
"""
|
||||
A base class that implements a visual representation of the model data.
|
||||
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.
|
||||
from anixstream.__main__ import AniXStreamApp
|
||||
|
||||
self.app: AniXStreamApp = MDApp.get_running_app() # type: ignore
|
||||
# Adding a view class as observer.
|
||||
self.model.add_observer(self)
|
||||
@@ -1 +0,0 @@
|
||||
from .media_card import MediaCard,MediaCardsContainer
|
||||
@@ -1,4 +0,0 @@
|
||||
from kivy.uix.modalview import ModalView
|
||||
|
||||
class AnimdlDialogPopup(ModalView):
|
||||
pass
|
||||
@@ -1,3 +0,0 @@
|
||||
<MDLabel>:
|
||||
allow_copy:True
|
||||
allow_selection:True
|
||||
@@ -1 +0,0 @@
|
||||
from .media_card import MediaCard,MediaCardsContainer
|
||||
@@ -1,2 +0,0 @@
|
||||
from .media_player import MediaPopupVideoPlayer
|
||||
from .media_popup import MediaPopup
|
||||
@@ -1,19 +0,0 @@
|
||||
<MediaCardsContainer>
|
||||
size_hint:1,None
|
||||
height:max(self.minimum_height,dp(350),container.minimum_height)
|
||||
container:container
|
||||
orientation: 'vertical'
|
||||
padding:"10dp"
|
||||
spacing:"5dp"
|
||||
MDLabel:
|
||||
text:root.list_name
|
||||
MDScrollView:
|
||||
size_hint:1,None
|
||||
height:container.minimum_height
|
||||
MDBoxLayout:
|
||||
id:container
|
||||
spacing:"10dp"
|
||||
padding:"0dp","10dp","100dp","10dp"
|
||||
size_hint:None,None
|
||||
height:self.minimum_height
|
||||
width:self.minimum_width
|
||||
@@ -1,10 +0,0 @@
|
||||
from kivy.uix.videoplayer import VideoPlayer
|
||||
|
||||
# TODO: make fullscreen exp better
|
||||
class MediaPopupVideoPlayer(VideoPlayer):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def on_fullscreen(self, instance, value):
|
||||
super().on_fullscreen(instance, value)
|
||||
# self.state = "pause"
|
||||
@@ -1,170 +0,0 @@
|
||||
#:import get_hex_from_color kivy.utils.get_hex_from_color
|
||||
#:set yellow [.9,.9,0,.9]
|
||||
|
||||
<SingleLineLabel@MDLabel>:
|
||||
shorten:True
|
||||
shorten_from:"right"
|
||||
adaptive_height:True
|
||||
|
||||
<PopupBoxLayout@MDBoxLayout>
|
||||
adaptive_height:True
|
||||
<Video>:
|
||||
fit_mode:"fill"
|
||||
|
||||
# TODO: subdivide each main component to its own file
|
||||
<MediaPopup>
|
||||
size_hint: None, None
|
||||
height: dp(530)
|
||||
width: dp(400)
|
||||
radius:[5,5,5,5]
|
||||
md_bg_color:self.theme_cls.backgroundColor
|
||||
anchor_y: 'top'
|
||||
player:player
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
MDRelativeLayout:
|
||||
size_hint_y: None
|
||||
height: dp(280)
|
||||
line_color:root.caller.has_trailer_color
|
||||
line_width:1
|
||||
MediaPopupVideoPlayer:
|
||||
id:player
|
||||
source: root.caller.trailer_url
|
||||
thumbnail:app.default_anime_image
|
||||
state:"play" if root.caller.trailer_url else "stop"
|
||||
# fit_mode:"fill"
|
||||
size_hint_y: None
|
||||
height: dp(280)
|
||||
PopupBoxLayout:
|
||||
padding: "10dp","5dp"
|
||||
spacing:"5dp"
|
||||
pos_hint: {'left': 1,'top': 1}
|
||||
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
|
||||
|
||||
PopupBoxLayout:
|
||||
padding:"5dp"
|
||||
pos_hint: {'bottom': 1}
|
||||
SingleLineLabel:
|
||||
text:root.caller.media_status
|
||||
opacity:.8
|
||||
halign:"left"
|
||||
font_style:"Label"
|
||||
role:"medium"
|
||||
bold:True
|
||||
pos_hint: {'center_y': .5}
|
||||
SingleLineLabel:
|
||||
text:root.caller.first_aired_on
|
||||
opacity:.8
|
||||
halign:"right"
|
||||
font_style:"Label"
|
||||
role:"medium"
|
||||
bold:True
|
||||
pos_hint: {'center_y': .5}
|
||||
# header
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding:"10dp"
|
||||
spacing:"10dp"
|
||||
PopupBoxLayout:
|
||||
PopupBoxLayout:
|
||||
pos_hint: {'center_y': 0.5}
|
||||
TooltipMDIconButton:
|
||||
tooltip_text:root.caller.title
|
||||
icon: "play-circle"
|
||||
on_press:
|
||||
root.dismiss()
|
||||
app.show_anime_screen(root.caller.anime_id,root.caller.screen.name)
|
||||
TooltipMDIconButton:
|
||||
tooltip_text:"Add to your anime list"
|
||||
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"
|
||||
TooltipMDIconButton:
|
||||
disabled:True
|
||||
tooltip_text:"Coming soon"
|
||||
icon: "bell-circle" if not(root.caller.is_in_my_notify) else "bell-check"
|
||||
PopupBoxLayout:
|
||||
pos_hint: {'center_y': 0.5}
|
||||
orientation: 'vertical'
|
||||
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
role:"small"
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Genres: "+"[/color]"+root.caller.genres
|
||||
markup:True
|
||||
PopupBoxLayout:
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
role:"small"
|
||||
markup:True
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Popularity: "+"[/color]"+root.caller.popularity
|
||||
SingleLineLabel:
|
||||
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
|
||||
adaptive_height:True
|
||||
# footer
|
||||
PopupBoxLayout:
|
||||
orientation:"vertical"
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
markup:True
|
||||
role:"small"
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Next Airing Episode: "+"[/color]"+root.caller.next_airing_episode
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
role:"small"
|
||||
markup:True
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Studios: " + "[/color]"+root.caller.studios
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
markup:True
|
||||
role:"small"
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Producers: " + "[/color]"+root.caller.producers
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
markup:True
|
||||
role:"small"
|
||||
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Tags: "+"[/color]"+root.caller.tags
|
||||
@@ -1,87 +0,0 @@
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.clock import Clock
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.modalview import ModalView
|
||||
|
||||
from kivymd.theming import ThemableBehavior
|
||||
from kivymd.uix.behaviors import (
|
||||
BackgroundColorBehavior,
|
||||
StencilBehavior,
|
||||
CommonElevationBehavior,
|
||||
HoverBehavior,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
self.player.bind(fullscreen=self.handle_clean_fullscreen_transition)
|
||||
|
||||
def open(self, *_args, **kwargs):
|
||||
"""Display the modal in the Window.
|
||||
|
||||
When the view is opened, it will be faded in with an animation. If you
|
||||
don't want the animation, use::
|
||||
|
||||
view.open(animation=False)
|
||||
|
||||
"""
|
||||
from kivy.core.window import Window
|
||||
|
||||
if self._is_open:
|
||||
return
|
||||
self._window = Window
|
||||
self._is_open = True
|
||||
self.dispatch("on_pre_open")
|
||||
Window.add_widget(self)
|
||||
Window.bind(on_resize=self._align_center, on_keyboard=self._handle_keyboard)
|
||||
self.center = self.caller.to_window(*self.caller.center)
|
||||
self.fbind("center", self._align_center)
|
||||
self.fbind("size", self._align_center)
|
||||
if kwargs.get("animation", True):
|
||||
ani = Animation(_anim_alpha=1.0, d=self._anim_duration)
|
||||
ani.bind(on_complete=lambda *_args: self.dispatch("on_open"))
|
||||
ani.start(self)
|
||||
else:
|
||||
self._anim_alpha = 1.0
|
||||
self.dispatch("on_open")
|
||||
|
||||
def _align_center(self, *_args):
|
||||
if self._is_open:
|
||||
self.center = self.caller.to_window(*self.caller.center)
|
||||
|
||||
def on_leave(self, *args):
|
||||
def _leave(dt):
|
||||
self.player.state = "stop"
|
||||
if self.player._video:
|
||||
self.player._video.unload()
|
||||
|
||||
if not self.hovering:
|
||||
self.dismiss()
|
||||
|
||||
Clock.schedule_once(_leave, 2)
|
||||
|
||||
def handle_clean_fullscreen_transition(self,instance,fullscreen):
|
||||
if not fullscreen:
|
||||
if not self._is_open:
|
||||
instance.state = "stop"
|
||||
if vid:=instance._video:
|
||||
vid.unload()
|
||||
else:
|
||||
instance.state = "stop"
|
||||
if vid:=instance._video:
|
||||
vid.unload()
|
||||
self.dismiss()
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<Tooltip>
|
||||
MDTooltipPlain:
|
||||
text:root.tooltip_text
|
||||
@@ -1,24 +0,0 @@
|
||||
<MediaCard>
|
||||
adaptive_height:True
|
||||
spacing:"5dp"
|
||||
image:"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx163270-oxwgbe43Cpog.jpg"
|
||||
size_hint_x: None
|
||||
width:dp(100)
|
||||
on_release:
|
||||
self.open()
|
||||
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
|
||||
SingleLineLabel:
|
||||
font_style:"Label"
|
||||
role:"medium"
|
||||
text:root.title
|
||||
max_lines:2
|
||||
halign:"center"
|
||||
color:self.theme_cls.secondaryColor
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
from kivy.properties import (
|
||||
ObjectProperty,
|
||||
StringProperty,
|
||||
BooleanProperty,
|
||||
ListProperty,
|
||||
NumericProperty,
|
||||
)
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.behaviors import HoverBehavior
|
||||
|
||||
from .components import MediaPopup
|
||||
|
||||
|
||||
class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout):
|
||||
screen = ObjectProperty()
|
||||
anime_id = NumericProperty()
|
||||
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()
|
||||
producers = StringProperty()
|
||||
studios = StringProperty()
|
||||
next_airing_episode = StringProperty()
|
||||
tags = StringProperty()
|
||||
stars = ListProperty([0, 0, 0, 0, 0, 0])
|
||||
cover_image_url = StringProperty()
|
||||
preview_image = StringProperty()
|
||||
has_trailer_color = ListProperty([.5, .5, .5, .5])
|
||||
|
||||
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
|
||||
|
||||
def on_enter(self):
|
||||
def _open_popup(dt):
|
||||
if self.hovering:
|
||||
window = self.get_parent_window()
|
||||
if 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.state = "stop"
|
||||
if popup.player._video:
|
||||
popup.player._video.unload()
|
||||
|
||||
def set_preview_image(self, image):
|
||||
self.preview_image = image
|
||||
|
||||
def set_trailer_url(self, trailer_url):
|
||||
self.trailer_url = trailer_url
|
||||
self.has_trailer_color = self.theme_cls.primaryColor
|
||||
|
||||
def open(self, *_):
|
||||
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, in_user_anime_list):
|
||||
if self.screen:
|
||||
if in_user_anime_list:
|
||||
self.screen.app.add_anime_to_user_anime_list(self.anime_id)
|
||||
else:
|
||||
self.screen.app.remove_anime_from_user_anime_list(self.anime_id)
|
||||
|
||||
def on_trailer_url(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class MediaCardsContainer(MDBoxLayout):
|
||||
container = ObjectProperty()
|
||||
list_name = StringProperty()
|
||||
@@ -1,45 +0,0 @@
|
||||
<CommonNavigationRailItem>
|
||||
MDNavigationRailItemIcon:
|
||||
icon:root.icon
|
||||
MDNavigationRailItemLabel:
|
||||
text: root.text
|
||||
|
||||
|
||||
<NavRail>:
|
||||
anchor:"top"
|
||||
type: "labeled"
|
||||
md_bg_color: self.theme_cls.secondaryContainerColor
|
||||
MDNavigationRailFabButton:
|
||||
icon: "home"
|
||||
on_press:
|
||||
root.screen.manager_screens.current = "home screen"
|
||||
CommonNavigationRailItem:
|
||||
icon: "magnify"
|
||||
text: "Search"
|
||||
on_press:
|
||||
root.screen.manager_screens.current = "search screen"
|
||||
|
||||
CommonNavigationRailItem:
|
||||
icon: "bookmark"
|
||||
text: "MyList"
|
||||
on_press:
|
||||
root.screen.manager_screens.current = "my list screen"
|
||||
CommonNavigationRailItem:
|
||||
icon: "download-circle"
|
||||
text: "Downloads"
|
||||
on_press:
|
||||
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"
|
||||
@@ -1,3 +0,0 @@
|
||||
<Tooltip>
|
||||
MDTooltipPlain:
|
||||
text:root.tooltip_text
|
||||
@@ -1,18 +0,0 @@
|
||||
<SearchBar>:
|
||||
pos_hint: {'center_x': 0.5,'top': 1}
|
||||
padding: "10dp"
|
||||
adaptive_height:True
|
||||
size_hint_x:.75
|
||||
spacing: '20dp'
|
||||
MDTextField:
|
||||
size_hint_x:1
|
||||
required:True
|
||||
on_text_validate:
|
||||
app.search_for_anime(args[0])
|
||||
MDTextFieldLeadingIcon:
|
||||
icon: "magnify"
|
||||
MDTextFieldHintText:
|
||||
text: "Search for anime"
|
||||
MDIconButton:
|
||||
pos_hint: {'center_y': 0.5}
|
||||
icon: "account-circle"
|
||||
@@ -1,50 +0,0 @@
|
||||
from anixstream.Controller import (
|
||||
SearchScreenController,
|
||||
HomeScreenController,
|
||||
MyListScreenController,
|
||||
AnimeScreenController,
|
||||
DownloadsScreenController,
|
||||
HelpScreenController,
|
||||
CrashLogScreenController,
|
||||
)
|
||||
from anixstream.Model import (
|
||||
HomeScreenModel,
|
||||
SearchScreenModel,
|
||||
MyListScreenModel,
|
||||
AnimeScreenModel,
|
||||
DownloadsScreenModel,
|
||||
HelpScreenModel,
|
||||
CrashLogScreenModel,
|
||||
)
|
||||
|
||||
|
||||
screens = {
|
||||
"home screen": {
|
||||
"model": HomeScreenModel,
|
||||
"controller": HomeScreenController,
|
||||
},
|
||||
"search screen": {
|
||||
"model": SearchScreenModel,
|
||||
"controller": SearchScreenController,
|
||||
},
|
||||
"my list screen": {
|
||||
"model": MyListScreenModel,
|
||||
"controller": MyListScreenController,
|
||||
},
|
||||
"anime screen": {
|
||||
"model": AnimeScreenModel,
|
||||
"controller": AnimeScreenController,
|
||||
},
|
||||
"crashlog screen": {
|
||||
"model": CrashLogScreenModel,
|
||||
"controller": CrashLogScreenController,
|
||||
},
|
||||
"downloads screen": {
|
||||
"model": DownloadsScreenModel,
|
||||
"controller": DownloadsScreenController,
|
||||
},
|
||||
"help screen": {
|
||||
"model": HelpScreenModel,
|
||||
"controller": HelpScreenController,
|
||||
},
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
os.environ["KIVY_VIDEO"] = "ffpyplayer"
|
||||
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
from subprocess import Popen
|
||||
import webbrowser
|
||||
|
||||
import plyer
|
||||
|
||||
from kivy.config import Config
|
||||
from kivy.resources import resource_find,resource_add_path,resource_remove_path
|
||||
# resource_add_path("_internal")
|
||||
|
||||
app_dir = os.path.dirname(__file__)
|
||||
|
||||
# test
|
||||
|
||||
# test_end
|
||||
|
||||
|
||||
# make sure we aint searching dist folder
|
||||
dist_folder = os.path.join(app_dir,"dist")
|
||||
resource_remove_path(dist_folder)
|
||||
|
||||
assets_folder = os.path.join(app_dir,"assets")
|
||||
resource_add_path(assets_folder)
|
||||
conigs_folder = os.path.join(app_dir,"configs")
|
||||
resource_add_path(conigs_folder)
|
||||
data_folder = os.path.join(app_dir,"data")
|
||||
resource_add_path(data_folder)
|
||||
|
||||
Config.set("graphics","width","1000")
|
||||
Config.set("graphics","minimum_width","1000")
|
||||
|
||||
Config.set('kivy', 'window_icon', resource_find("logo.ico"))
|
||||
Config.write()
|
||||
|
||||
# from kivy.core.window import Window
|
||||
|
||||
from kivy.loader import Loader
|
||||
|
||||
Loader.num_workers = 5
|
||||
Loader.max_upload_per_frame = 10
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.uix.screenmanager import ScreenManager, FadeTransition
|
||||
from kivy.uix.settings import SettingsWithSidebar, Settings
|
||||
|
||||
from kivymd.icon_definitions import md_icons
|
||||
from kivymd.app import MDApp
|
||||
|
||||
from anixstream.View.screens import screens
|
||||
from anixstream.libs.animdl import AnimdlApi
|
||||
from anixstream.Utility import (
|
||||
themes_available,
|
||||
show_notification,
|
||||
user_data_helper,
|
||||
animdl_config_manager,
|
||||
)
|
||||
from anixstream.Utility.utils import write_crash
|
||||
|
||||
# Ensure the user data fields exist
|
||||
if not (user_data_helper.user_data.exists("user_anime_list")):
|
||||
user_data_helper.update_user_anime_list([])
|
||||
|
||||
if not (user_data_helper.yt_cache.exists("yt_stream_links")):
|
||||
user_data_helper.update_anime_trailer_cache([])
|
||||
|
||||
# TODO: Confirm data integrity from user_data and yt_cache
|
||||
|
||||
|
||||
class AniXStreamApp(MDApp):
|
||||
queue = Queue()
|
||||
downloads_queue = Queue()
|
||||
animdl_streaming_subprocess: Popen | None = None
|
||||
default_anime_image = resource_find(random.choice(["default_1.jpg","default.jpg"]))
|
||||
default_banner_image = resource_find(random.choice(["banner_1.jpg","banner.jpg"]))
|
||||
# default_video = resource_find("Billyhan_When you cant afford Crunchyroll to watch anime.mp4")
|
||||
|
||||
def worker(self, queue: Queue):
|
||||
while True:
|
||||
task = queue.get() # task should be a function
|
||||
try:
|
||||
task()
|
||||
except Exception as e:
|
||||
show_notification("An error occured while streaming", f"{e}")
|
||||
self.queue.task_done()
|
||||
|
||||
def downloads_worker(self, queue: Queue):
|
||||
while True:
|
||||
download_task = queue.get() # task should be a function
|
||||
try:
|
||||
download_task()
|
||||
except Exception as e:
|
||||
show_notification("An error occured while downloading", f"{e}")
|
||||
self.downloads_queue.task_done()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.icon = resource_find("logo.png")
|
||||
|
||||
self.load_all_kv_files(self.directory)
|
||||
self.theme_cls.theme_style = "Dark"
|
||||
self.theme_cls.primary_palette = "Lightcoral"
|
||||
self.manager_screens = ScreenManager()
|
||||
self.manager_screens.transition = FadeTransition()
|
||||
|
||||
# initialize worker
|
||||
self.worker_thread = Thread(target=self.worker, args=(self.queue,))
|
||||
self.worker_thread.daemon = True
|
||||
self.worker_thread.start()
|
||||
Logger.info("AniXStream:Successfully started background tasks worker")
|
||||
|
||||
# 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()
|
||||
Logger.info("AniXStream:Successfully started download worker")
|
||||
|
||||
def build(self) -> ScreenManager:
|
||||
self.settings_cls = SettingsWithSidebar
|
||||
|
||||
self.generate_application_screens()
|
||||
|
||||
if config := self.config:
|
||||
if theme_color := config.get("Preferences", "theme_color"):
|
||||
self.theme_cls.primary_palette = theme_color
|
||||
if theme_style := config.get("Preferences", "theme_style"):
|
||||
self.theme_cls.theme_style = theme_style
|
||||
|
||||
self.anime_screen = self.manager_screens.get_screen("anime screen")
|
||||
self.search_screen = self.manager_screens.get_screen("search screen")
|
||||
self.download_screen = self.manager_screens.get_screen("downloads screen")
|
||||
self.home_screen = self.manager_screens.get_screen("home screen")
|
||||
return self.manager_screens
|
||||
|
||||
def on_start(self, *args):
|
||||
if self.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore
|
||||
Clock.schedule_once(lambda _:self.home_screen.controller.populate_home_screen(),1)
|
||||
|
||||
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)
|
||||
|
||||
def build_config(self, config):
|
||||
|
||||
# General settings setup
|
||||
if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore
|
||||
downloads_dir = os.path.join(vid_path, "anixstream")
|
||||
if not os.path.exists(downloads_dir):
|
||||
os.mkdir(downloads_dir)
|
||||
else:
|
||||
downloads_dir = os.path.join(".", "videos")
|
||||
if not os.path.exists(downloads_dir):
|
||||
os.mkdir(downloads_dir)
|
||||
config.setdefaults(
|
||||
"Preferences",
|
||||
{
|
||||
"theme_color": "Cyan",
|
||||
"theme_style": "Dark",
|
||||
"downloads_dir": downloads_dir,
|
||||
"is_startup_anime_enable": False,
|
||||
},
|
||||
)
|
||||
|
||||
# animdl config settings setup
|
||||
animdl_config = animdl_config_manager.get_animdl_config()
|
||||
config.setdefaults(
|
||||
"Providers",
|
||||
{
|
||||
"default_provider": animdl_config["default_provider"],
|
||||
},
|
||||
)
|
||||
config.setdefaults(
|
||||
"Quality",
|
||||
{
|
||||
"quality_string": animdl_config["quality_string"],
|
||||
},
|
||||
)
|
||||
config.setdefaults(
|
||||
"PlayerSelection",
|
||||
{
|
||||
"default_player": animdl_config["default_player"],
|
||||
},
|
||||
)
|
||||
|
||||
def build_settings(self, settings: Settings):
|
||||
settings.add_json_panel(
|
||||
"Settings", self.config, resource_find("general_settings_panel.json")
|
||||
)
|
||||
settings.add_json_panel(
|
||||
"Animdl Config", self.config, resource_find("animdl_config_panel.json")
|
||||
)
|
||||
|
||||
def on_config_change(self, config, section, key, value):
|
||||
# TODO: Change to match case
|
||||
if section == "Preferences":
|
||||
match key:
|
||||
case "theme_color":
|
||||
if value in themes_available:
|
||||
self.theme_cls.primary_palette = value
|
||||
else:
|
||||
Logger.warning(
|
||||
"AniXStream Settings: An invalid theme has been entered and will be ignored"
|
||||
)
|
||||
config.set("Preferences", "theme_color", "Cyan")
|
||||
config.write()
|
||||
case "theme_style":
|
||||
self.theme_cls.theme_style = value
|
||||
elif section == "Providers":
|
||||
animdl_config_manager.update_animdl_config("default_provider", value)
|
||||
elif section == "Quality":
|
||||
animdl_config_manager.update_animdl_config("quality_string", value)
|
||||
elif section == "PlayerSelection":
|
||||
animdl_config_manager.update_animdl_config("default_player", value)
|
||||
|
||||
def on_stop(self):
|
||||
del self.downloads_worker_thread
|
||||
if self.animdl_streaming_subprocess:
|
||||
self.stop_streaming = True
|
||||
self.animdl_streaming_subprocess.terminate()
|
||||
del self.worker_thread
|
||||
|
||||
Logger.info("Animdl:Successfully terminated existing animdl subprocess")
|
||||
|
||||
# custom methods
|
||||
def search_for_anime(self, search_field, **kwargs):
|
||||
if self.manager_screens.current != "search screen":
|
||||
self.manager_screens.current = "search screen"
|
||||
self.search_screen.handle_search_for_anime(search_field, **kwargs)
|
||||
|
||||
def add_anime_to_user_anime_list(self, id: int):
|
||||
updated_list = user_data_helper.get_user_anime_list()
|
||||
updated_list.append(id)
|
||||
user_data_helper.update_user_anime_list(updated_list)
|
||||
|
||||
def remove_anime_from_user_anime_list(self, id: int):
|
||||
updated_list = user_data_helper.get_user_anime_list()
|
||||
if updated_list.count(id):
|
||||
updated_list.remove(id)
|
||||
user_data_helper.update_user_anime_list(updated_list)
|
||||
|
||||
def add_anime_to_user_downloads_list(self, id: int):
|
||||
updated_list = user_data_helper.get_user_downloads()
|
||||
updated_list.append(id)
|
||||
user_data_helper.get_user_downloads()
|
||||
|
||||
def show_anime_screen(self, id: int, caller_screen_name: str):
|
||||
self.manager_screens.current = "anime screen"
|
||||
self.anime_screen.controller.update_anime_view(id, caller_screen_name)
|
||||
|
||||
def download_anime_complete(
|
||||
self, successful_downloads: list, failed_downloads: list, anime_title: str
|
||||
):
|
||||
show_notification(
|
||||
f"Finished Dowloading {anime_title}",
|
||||
f"There were {len(successful_downloads)} successful downloads and {len(failed_downloads)} failed downloads",
|
||||
)
|
||||
Logger.info(
|
||||
f"Downloader:Finished Downloading {anime_title} and there were {len(failed_downloads)} failed downloads"
|
||||
)
|
||||
|
||||
def download_anime(self, anime_id: int, default_cmds: dict):
|
||||
show_notification(
|
||||
"New Download Task Queued",
|
||||
f"{default_cmds.get('title')} has been queued for downloading",
|
||||
)
|
||||
self.add_anime_to_user_downloads_list(anime_id)
|
||||
|
||||
# TODO:Add custom download cmds functionality
|
||||
on_progress = lambda *args: self.download_screen.on_episode_download_progress(
|
||||
*args
|
||||
)
|
||||
output_path = self.config.get("Preferences", "downloads_dir") # type: ignore
|
||||
self.download_screen.on_new_download_task(
|
||||
default_cmds["title"], default_cmds.get("episodes_range")
|
||||
)
|
||||
if episodes_range := default_cmds.get("episodes_range"):
|
||||
download_task = lambda: AnimdlApi.download_anime_by_title(
|
||||
default_cmds["title"],
|
||||
on_progress,
|
||||
lambda anime_title, episode: show_notification(
|
||||
"Finished installing an episode", f"{anime_title}-{episode}"
|
||||
),
|
||||
self.download_anime_complete,
|
||||
output_path,
|
||||
episodes_range,
|
||||
) # ,default_cmds["quality"]
|
||||
self.downloads_queue.put(download_task)
|
||||
Logger.info(
|
||||
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
|
||||
)
|
||||
else:
|
||||
download_task = lambda: AnimdlApi.download_anime_by_title(
|
||||
default_cmds["title"],
|
||||
on_progress,
|
||||
lambda anime_title, episode: show_notification(
|
||||
"Finished installing an episode", f"{anime_title}-{episode}"
|
||||
),
|
||||
self.download_anime_complete,
|
||||
output_path,
|
||||
) # ,default_cmds.get("quality")
|
||||
self.downloads_queue.put(download_task)
|
||||
Logger.info(
|
||||
f"Downloader:Successfully Queued {default_cmds['title']} for downloading"
|
||||
)
|
||||
|
||||
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
|
||||
"""
|
||||
try:
|
||||
if anime := AnimdlApi.get_anime_url_by_title(title_):
|
||||
title, link = anime
|
||||
parsed_link = f"https://allmanga.to/bangumi/{link.split('/')[-1]}"
|
||||
else:
|
||||
Logger.error(
|
||||
f"AniXStream:Failed to open {title_} in browser on allanime site"
|
||||
)
|
||||
show_notification(
|
||||
"Failure", f"Failed to open {title_} in browser on allanime site"
|
||||
)
|
||||
if webbrowser.open(parsed_link):
|
||||
Logger.info(
|
||||
f"AniXStream:Successfully opened {title} in browser allanime site"
|
||||
)
|
||||
show_notification(
|
||||
"Success", f"Successfully opened {title} in browser allanime site"
|
||||
)
|
||||
else:
|
||||
Logger.error(
|
||||
f"AniXStream:Failed to open {title} in browser on allanime site"
|
||||
)
|
||||
show_notification(
|
||||
"Failure", f"Failed to open {title} in browser on allanime site"
|
||||
)
|
||||
except Exception as e:
|
||||
show_notification("Something went wrong",f"{e}")
|
||||
|
||||
def stream_anime_with_custom_input_cmds(self, *cmds):
|
||||
self.animdl_streaming_subprocess = (
|
||||
AnimdlApi._run_animdl_command_and_get_subprocess(["stream", *cmds])
|
||||
)
|
||||
|
||||
def stream_anime_by_title_with_animdl(
|
||||
self, title, episodes_range: str | None = None
|
||||
):
|
||||
self.stop_streaming = False
|
||||
self.animdl_streaming_subprocess = AnimdlApi.stream_anime_by_title_on_animdl(
|
||||
title, episodes_range
|
||||
)
|
||||
|
||||
def stream_anime_with_mpv(
|
||||
self, title, episodes_range: str | None = None, quality: str = "best"
|
||||
):
|
||||
self.stop_streaming = False
|
||||
streams = AnimdlApi.stream_anime_with_mpv(title, episodes_range, quality)
|
||||
# TODO: End mpv child process properly
|
||||
for stream in streams:
|
||||
try:
|
||||
if isinstance(stream,str):
|
||||
show_notification("Failed to stream current episode",f"{stream}")
|
||||
continue
|
||||
self.animdl_streaming_subprocess = stream
|
||||
|
||||
for line in self.animdl_streaming_subprocess.stderr: # type: ignore
|
||||
if self.stop_streaming:
|
||||
if stream:
|
||||
stream.terminate()
|
||||
stream.kill()
|
||||
del stream
|
||||
return
|
||||
except Exception as e:
|
||||
show_notification("Something went wrong while streaming",f"{e}")
|
||||
|
||||
def watch_on_animdl(
|
||||
self,
|
||||
stream_with_mpv_options: 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.kill()
|
||||
self.stop_streaming = True
|
||||
|
||||
if stream_with_mpv_options:
|
||||
stream_func = lambda: self.stream_anime_with_mpv(
|
||||
stream_with_mpv_options["title"],
|
||||
stream_with_mpv_options.get("episodes_range"),
|
||||
stream_with_mpv_options["quality"],
|
||||
)
|
||||
self.queue.put(stream_func)
|
||||
|
||||
Logger.info(
|
||||
f"Animdl:Successfully started to stream {stream_with_mpv_options['title']}"
|
||||
)
|
||||
else:
|
||||
stream_func = lambda: self.stream_anime_with_custom_input_cmds(
|
||||
*custom_options
|
||||
)
|
||||
self.queue.put(stream_func)
|
||||
show_notification("Streamer", "Started streaming")
|
||||
|
||||
|
||||
def run_app():
|
||||
AniXStreamApp().run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run_app()
|
||||
except Exception as e:
|
||||
write_crash(e)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 291 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user