├── backend
├── config
│ ├── config.json
│ └── __init__.py
├── errors
│ ├── __init__.py
│ └── http_error.py
├── video
│ ├── __init__.py
│ ├── streamer
│ │ ├── __init__.py
│ │ └── stream.py
│ ├── library
│ │ ├── __init__.py
│ │ └── library.py
│ └── downloader
│ │ ├── __init__.py
│ │ └── msg_system.py
├── scraper
│ ├── sauce.txt
│ ├── __init__.py
│ ├── hentai_scraper.py
│ ├── base.py
│ ├── mal.py
│ └── manga_scraper.py
├── defaults
│ ├── def_manga.png
│ └── kaicons.jpg
├── middleware
│ ├── __init__.py
│ ├── ErrorHandlerMiddleware.py
│ └── requestValidator.py
├── utils
│ ├── __init__.py
│ ├── cleanup.py
│ ├── master_m3u8.py
│ ├── headers.py
│ ├── video_metadata.py
│ ├── path_validator.py
│ ├── staticfiles.py
│ └── init_db.py
├── sql_queries
│ ├── progress_tracker.sql
│ └── watchlist.sql
└── LiSA.py
├── .gitattributes
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── images
│ └── loader_logo.png
├── loader.html
├── manifest.json
├── css
│ └── loader.css
└── index.html
├── FUNDING.yml
├── requirements.txt
├── src
├── assets
│ └── img
│ │ ├── mpv.png
│ │ ├── vlc.png
│ │ ├── not-found.png
│ │ ├── loader-serch.gif
│ │ ├── home_screen_logo.jpg
│ │ └── home_screen_logo.png
├── store
│ ├── constants
│ │ ├── downloadConstants.ts
│ │ └── animeConstants.ts
│ ├── actions
│ │ ├── downloadActions.tsx
│ │ └── animeActions.ts
│ ├── store.ts
│ └── reducers
│ │ ├── downloadReducers.ts
│ │ └── animeReducers.ts
├── screens
│ ├── settingScreen.tsx
│ ├── exploreScreen.tsx
│ ├── homeScreen.tsx
│ ├── inbuiltPlayerScreen.tsx
│ └── downloadsScreen.tsx
├── components
│ ├── back-button
│ │ ├── index.tsx
│ │ └── back-button.css
│ ├── downloadList.tsx
│ ├── metadata-popup.tsx
│ ├── network-error.tsx
│ ├── navbar.tsx
│ ├── externalPopup.tsx
│ ├── downloadItem.tsx
│ ├── ep-popover.tsx
│ ├── card.tsx
│ ├── search-result-card.tsx
│ ├── video-player.tsx
│ └── paginateCard.tsx
├── utils
│ ├── axios.ts
│ └── formatBytes.ts
├── styles
│ ├── index.css
│ ├── theme.ts
│ └── App.css
├── context
│ └── socket.tsx
├── hooks
│ ├── useNetworkStatus.tsx
│ └── useSocketStatus.tsx
├── index.tsx
└── App.tsx
├── demo_images
├── ss_search.png
├── ss_explore.png
├── ss_anime_details.png
├── ss_play_episode.png
└── ss_download_manager.png
├── .prettierrc
├── utilities
├── deb
│ └── images
│ │ ├── icon.ico
│ │ ├── banner.png
│ │ └── background.png
├── dmg
│ └── images
│ │ ├── icon.icns
│ │ └── background.png
└── msi
│ └── images
│ ├── icon.ico
│ ├── banner.png
│ └── background.png
├── CONTRIBUTING.md
├── renderer.js
├── .gitignore
├── preload.js
├── tsconfig.json
├── scripts
├── clean.js
├── build.js
├── start.js
├── dispatch.js
└── package.js
├── LICENSE
├── LiSA.spec
├── package.json
├── README.md
└── main.js
/backend/config/config.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/backend/errors/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/video/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/video/streamer/__init__.py:
--------------------------------------------------------------------------------
1 | from .stream import Stream
--------------------------------------------------------------------------------
/backend/video/library/__init__.py:
--------------------------------------------------------------------------------
1 | from .library import DBLibrary, Library
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/backend/scraper/sauce.txt:
--------------------------------------------------------------------------------
1 | Rule34videos
2 | Cartoonpornvideos
3 | Oppaistream
4 | HentaiHaven
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [cosmicoppai, nishantchaware, heyharshjaiswal]
2 | custom: ["https://paypal.me/SayAnime"]
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/requirements.txt
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/assets/img/mpv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/mpv.png
--------------------------------------------------------------------------------
/src/assets/img/vlc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/vlc.png
--------------------------------------------------------------------------------
/backend/video/downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .downloader import Downloader, DownloadManager, MangaDownloader, VideoDownloader
2 |
--------------------------------------------------------------------------------
/demo_images/ss_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/demo_images/ss_search.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "printWidth": 100,
4 | "proseWrap": "preserve",
5 | "bracketSameLine": true
6 | }
--------------------------------------------------------------------------------
/demo_images/ss_explore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/demo_images/ss_explore.png
--------------------------------------------------------------------------------
/backend/defaults/def_manga.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/backend/defaults/def_manga.png
--------------------------------------------------------------------------------
/backend/defaults/kaicons.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/backend/defaults/kaicons.jpg
--------------------------------------------------------------------------------
/public/images/loader_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/public/images/loader_logo.png
--------------------------------------------------------------------------------
/src/assets/img/not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/not-found.png
--------------------------------------------------------------------------------
/utilities/deb/images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/deb/images/icon.ico
--------------------------------------------------------------------------------
/utilities/dmg/images/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/dmg/images/icon.icns
--------------------------------------------------------------------------------
/utilities/msi/images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/msi/images/icon.ico
--------------------------------------------------------------------------------
/demo_images/ss_anime_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/demo_images/ss_anime_details.png
--------------------------------------------------------------------------------
/demo_images/ss_play_episode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/demo_images/ss_play_episode.png
--------------------------------------------------------------------------------
/src/assets/img/loader-serch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/loader-serch.gif
--------------------------------------------------------------------------------
/utilities/deb/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/deb/images/banner.png
--------------------------------------------------------------------------------
/utilities/msi/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/msi/images/banner.png
--------------------------------------------------------------------------------
/backend/middleware/__init__.py:
--------------------------------------------------------------------------------
1 | from .ErrorHandlerMiddleware import ErrorHandlerMiddleware
2 | from .requestValidator import RequestValidator
3 |
--------------------------------------------------------------------------------
/demo_images/ss_download_manager.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/demo_images/ss_download_manager.png
--------------------------------------------------------------------------------
/src/assets/img/home_screen_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/home_screen_logo.jpg
--------------------------------------------------------------------------------
/src/assets/img/home_screen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/src/assets/img/home_screen_logo.png
--------------------------------------------------------------------------------
/utilities/deb/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/deb/images/background.png
--------------------------------------------------------------------------------
/utilities/dmg/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/dmg/images/background.png
--------------------------------------------------------------------------------
/utilities/msi/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developerrahulofficial/jennie-anime-player/HEAD/utilities/msi/images/background.png
--------------------------------------------------------------------------------
/backend/scraper/__init__.py:
--------------------------------------------------------------------------------
1 | from .scraper import Animepahe, Anime
2 | from .manga_scraper import Manga, MangaKatana
3 | from .base import Scraper, Proxy
4 | from .mal import MyAL
5 |
--------------------------------------------------------------------------------
/backend/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .init_db import DB
2 | from .cleanup import remove_folder, remove_file
3 | from .path_validator import validate_path
4 | from .staticfiles import CustomStaticFiles
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for help improving LiSA. All kinds of contributions are welcome. We are open to suggestions!
4 |
5 | Please submit an Issue or even better a PR and We'll review :)
--------------------------------------------------------------------------------
/src/store/constants/downloadConstants.ts:
--------------------------------------------------------------------------------
1 | export const ANIME_DOWNLOAD_REQUEST = "ANIME_DOWNLOAD_REQUEST";
2 | export const ANIME_DOWNLOAD_SUCCESS = "ANIME_DOWNLOAD_SUCCESS";
3 | export const ANIME_DOWNLOAD_FAIL = "ANIME_DOWNLOAD_FAIL";
4 |
--------------------------------------------------------------------------------
/backend/scraper/hentai_scraper.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from abc import abstractmethod
3 | from bs4 import BeautifulSoup
4 | from typing import Dict, List, Any
5 | from config import ServerConfig
6 | from utils.headers import get_headers
7 | import re
8 | from .base import Scraper
--------------------------------------------------------------------------------
/src/screens/settingScreen.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | export default function SettingScreen() {
4 | return (
5 |
6 | Setting
7 | BACK
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/renderer.js:
--------------------------------------------------------------------------------
1 | // This file is required by the index.html file and will
2 | // be executed in the renderer process for that window.
3 | // No Node.js APIs are available in this process because
4 | // `nodeIntegration` is turned off. Use `preload.js` to
5 | // selectively enable features needed in the rendering
6 | // process.
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.mp4
3 | *.env
4 | run_server.sh
5 | *.m3u8
6 |
7 | ui/node_modules
8 | node_modules
9 | node_modules/
10 |
11 | dist/
12 |
13 |
14 | build/
15 | resources/
16 | .eslintcache
17 |
18 | backend/video/segments
19 |
20 | downloads
21 |
22 | *.exe
23 | backend/lisa
24 |
25 | env/
26 | venv/
27 |
28 | .idea
--------------------------------------------------------------------------------
/backend/utils/cleanup.py:
--------------------------------------------------------------------------------
1 | from shutil import rmtree
2 | from os import remove
3 |
4 |
5 | def remove_folder(file_location: str):
6 | try:
7 | rmtree(file_location)
8 | except FileNotFoundError or NotADirectoryError:
9 | ...
10 |
11 |
12 | def remove_file(file_location: str):
13 | try:
14 | remove(file_location)
15 | except FileNotFoundError:
16 | ...
--------------------------------------------------------------------------------
/src/components/back-button/index.tsx:
--------------------------------------------------------------------------------
1 | import "./back-button.css";
2 |
3 | export default function BackButton() {
4 | return (
5 |
6 |
7 |
8 | Back
9 |
10 |
11 | );
12 | };
--------------------------------------------------------------------------------
/src/utils/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const server = axios.create({
4 | baseURL: process.env.REACT_APP_SERVER_URL,
5 | });
6 |
7 | server.interceptors.request.use(
8 | (config) => {
9 | try {
10 | return config;
11 | } catch {
12 | return null;
13 | }
14 | },
15 | (error) => Promise.reject(error)
16 | );
17 |
18 | export default server;
19 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
4 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
9 | code {
10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/formatBytes.ts:
--------------------------------------------------------------------------------
1 | export function formatBytes(bytes: number, decimals: number = 2) {
2 | if (bytes === 0) return "0 mb";
3 |
4 | const k = 1024;
5 | const dm = decimals < 0 ? 0 : decimals;
6 | const sizes = ["mb", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
7 |
8 | const i = Math.floor(Math.log(bytes) / Math.log(k));
9 |
10 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
11 | };
12 |
--------------------------------------------------------------------------------
/backend/sql_queries/progress_tracker.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS progress_tracker (
2 | id integer PRIMARY KEY,
3 | type varchar NOT NULL DEFAULT 'video', -- can be of type video or image
4 | series_name varchar NOT NULL ,
5 | file_name varchar NOT NULL UNIQUE,
6 | status char(50) NOT NULL,
7 | total_size int NOT NULL,
8 | manifest_file_path varchar NOT NULL UNIQUE ,
9 | file_location varchar default NULL UNIQUE,
10 | created_on datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
11 | );
12 |
--------------------------------------------------------------------------------
/src/context/socket.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | const W3CWebSocket = require("websocket").w3cwebsocket;
4 |
5 | export const client = new W3CWebSocket(process.env.REACT_APP_SOCKET_URL);
6 |
7 | export const SocketContext = createContext<{
8 | readyState?: any,
9 | send?: any,
10 | close?: any
11 | }>({});
12 |
13 | export function useSocketContext() {
14 | const context = useContext(SocketContext)
15 | if (!context) throw Error('useSocketContext must be used inside an SocketContextProvider')
16 | return context
17 | }
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | // All of the Node.js APIs are available in the preload process.
2 | // It has the same sandbox as a Chrome extension.
3 | window.addEventListener("DOMContentLoaded", () => {
4 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
5 |
6 | const replaceText = (selector, text) => {
7 | const element = document.getElementById(selector);
8 | if (element) element.innerText = text;
9 | };
10 |
11 | for (const type of ["chrome", "node", "electron"]) {
12 | replaceText(`${type}-version`, process.versions[type]);
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/backend/sql_queries/watchlist.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS watchlist (
2 | anime_id integer PRIMARY KEY ,
3 | jp_name varchar NOT NULL UNIQUE ,
4 | no_of_episodes NOT NULL CHECK ( no_of_episodes > 0 ),
5 | type char(25) NOT NULL ,
6 | status char(50) NOT NULL, -- airing status of anime
7 | season char(50) NOT NULL ,
8 | year integer NOT NULL CHECK ( year >= 1900 ),
9 | score integer NOT NULL CHECK ( score >= 0 ),
10 | poster varchar NOT NULL,
11 | ep_details varchar NOT NULL UNIQUE,
12 | created_on datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
13 | );
14 |
--------------------------------------------------------------------------------
/public/loader.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |

10 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "commonjs",
5 | "jsx": "react-jsx",
6 | "allowJs": true,
7 | "checkJs": true,
8 | "skipLibCheck": true,
9 | "esModuleInterop": true,
10 | "sourceMap": true,
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 |
15 | "baseUrl": ".",
16 | "paths": {
17 | "@public/*": ["./public/*"]
18 | }
19 | },
20 | "include": ["src/**/*"],
21 | "exclude": ["node_modules", "backend"]
22 | }
23 |
--------------------------------------------------------------------------------
/backend/utils/master_m3u8.py:
--------------------------------------------------------------------------------
1 | from .video_metadata import get_metadata
2 | from typing import List
3 |
4 | master_m3u8 = """#EXTM3U\n#EXT-X-VERSION:3\n"""
5 |
6 |
7 | def build_master_manifest(kwik_urls: List[str]) -> str:
8 | m3u8 = master_m3u8
9 | try:
10 | for kwik_url in kwik_urls:
11 | link, p_res = kwik_url.split("-")
12 | video_res, bandwith = get_metadata(int(p_res))
13 | m3u8 += f"#EXT-X-STREAM-INF:BANDWITH={bandwith},RESOLUTION={video_res[0]}x{video_res[1]}\n{link}\n"
14 | except ValueError:
15 | raise ValueError("Invalid kwik_url")
16 |
17 | return m3u8
18 |
--------------------------------------------------------------------------------
/backend/utils/headers.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Any
2 |
3 |
4 | def get_headers(extra: Dict[str, Any] = {}) -> Dict[str, str]:
5 | headers = {
6 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
7 | "accept-language": "en-GB,en;q=0.9,ja-JP;q=0.8,ja;q=0.7,en-US;q=0.6",
8 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
9 | }
10 | for key, val in extra.items():
11 | headers[key] = val
12 | return headers
13 |
14 |
--------------------------------------------------------------------------------
/backend/utils/video_metadata.py:
--------------------------------------------------------------------------------
1 | from math import ceil
2 |
3 |
4 | # 16:9 aspect ratio resolution
5 |
6 | def get_video_resolution(progressive_resolution: int) -> (int, int): # width, height
7 | return ceil(16 / 9 * progressive_resolution), progressive_resolution
8 |
9 |
10 | def get_bandwith(width: int, height: int, fps: int = 30, bpp: float = 0.1) -> int:
11 | return int(round(width*height/10**6, 1)*fps*24*1000)
12 |
13 |
14 | def get_metadata(progressive_resolution: int) -> (str, int):
15 | video_res = get_video_resolution(progressive_resolution)
16 | bandwith = get_bandwith(video_res[0], video_res[1])
17 |
18 | return video_res, bandwith
19 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from "@chakra-ui/react";
2 |
3 | export const theme = extendTheme({
4 | initialColorMode: "light",
5 | useSystemColorMode: false,
6 | fonts: {
7 | heading: `'Montserrat', sans-serif`,
8 | body: `'Montserrat', sans-serif`,
9 | },
10 | styles: {
11 | global: {
12 | body: {
13 | maxHeight: "100vh",
14 | },
15 | },
16 | },
17 | colors: {
18 | brand: {
19 | 100: "#edf2f7",
20 | 900: "#1a202c",
21 | },
22 | font: {
23 | main: "#edf2f7",
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/hooks/useNetworkStatus.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useNetworkStatus = () => {
4 | const [isOnline, setIsOnline] = useState(true);
5 |
6 | useEffect(() => {
7 | const interval = setInterval(() => {
8 | fetch("https://www.google.com/", {
9 | mode: "no-cors",
10 | })
11 | .then(() => !isOnline && setIsOnline(true))
12 | .catch(() => isOnline && setIsOnline(false));
13 | }, 5000);
14 |
15 | return () => clearInterval(interval);
16 | }, [isOnline]);
17 |
18 | return { isOnline };
19 | };
20 |
21 | export default useNetworkStatus;
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Lisa",
3 | "name": "Lisa- Anime Engine",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/backend/middleware/ErrorHandlerMiddleware.py:
--------------------------------------------------------------------------------
1 | from starlette.middleware.base import BaseHTTPMiddleware
2 | from starlette.requests import Request
3 | from starlette.responses import Response
4 | from starlette.middleware.base import RequestResponseEndpoint
5 | from aiohttp import ClientResponseError
6 | from errors.http_error import service_unavailable_503
7 |
8 |
9 | class ErrorHandlerMiddleware(BaseHTTPMiddleware):
10 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
11 | try:
12 | return await call_next(request)
13 | except ClientResponseError:
14 | return await service_unavailable_503(request, msg="Remote server unreachable, please check your internet connection or try again after sometime")
15 |
--------------------------------------------------------------------------------
/src/components/downloadList.tsx:
--------------------------------------------------------------------------------
1 | import DownloadItem from "./downloadItem";
2 |
3 | export default function DownloadList(props) {
4 | return (
5 | <>
6 | {props?.filesStatus &&
7 | Object.entries(props?.filesStatus).map(([key, value]) => {
8 | return (
9 |
16 | );
17 | })
18 | }
19 | >
20 | );
21 | };
--------------------------------------------------------------------------------
/src/hooks/useSocketStatus.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useSocketStatus = () => {
4 | const [isSocketConnected, setIsSocketConnected] = useState(false);
5 |
6 | useEffect(() => {
7 | console.log("useSocketStatus", isSocketConnected);
8 |
9 | const interval = setInterval(() => {
10 | fetch("http://localhost:6969/", {
11 | mode: "no-cors",
12 | })
13 | .then(() => {
14 | setIsSocketConnected(true);
15 | clearInterval(interval);
16 | })
17 | .catch(() => isSocketConnected && setIsSocketConnected(false));
18 | }, 5000);
19 |
20 | return () => clearInterval(interval);
21 | }, [isSocketConnected]);
22 |
23 | return { isSocketConnected };
24 | };
25 |
26 | export default useSocketStatus;
27 |
--------------------------------------------------------------------------------
/backend/utils/path_validator.py:
--------------------------------------------------------------------------------
1 | from platform import system
2 | from sys import modules
3 | from typing import List
4 |
5 | _WINDOWS_INVALID_CHAR = ["/", "\\", "<", ">", ".", ":", '"', "|", "?", "*"]
6 | _WINDOWS_INVALID_ENDINGS = ". "
7 | _LINUX_INVALID_CHAR = ["/", ]
8 | _LINUX_INVALID_ENDINGS = ""
9 | _MAC_INVALID_CHAR = [":", "/"]
10 | _MAC_INVALID_ENDINGS = ":/"
11 |
12 |
13 | def validate_path(paths: List[str]) -> List[str]:
14 | invalid_chars: List[str] = getattr(modules[__name__], f"_{system().upper()}_INVALID_CHAR")
15 | invalid_endings: str = getattr(modules[__name__], f"_{system().upper()}_INVALID_ENDINGS")
16 |
17 | for idx, path in enumerate(paths):
18 | path = path.replace(" ", "-").rstrip(invalid_endings)
19 | for illegal_char in invalid_chars:
20 | path = path.replace(illegal_char, "")
21 | paths[idx] = path
22 |
23 | return paths
24 |
--------------------------------------------------------------------------------
/public/css/loader.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | overflow: hidden;
6 | }
7 | .circle {
8 | position: relative;
9 | width: 250px;
10 | height: 250px;
11 | overflow: hidden;
12 | }
13 | .circle svg {
14 | fill: none;
15 | stroke: #8a3ab9;
16 | stroke-linecap: round;
17 | stroke-width: 2;
18 | stroke-dasharray: 1;
19 | stroke-dashoffset: 0;
20 | animation: stroke-draw 4s ease-out infinite alternate;
21 | }
22 | .circle img {
23 | position: absolute;
24 | left: 50%;
25 | top: 50%;
26 | transform: translate(-50%, -50%);
27 | width: 200px;
28 | border-radius: 50%;
29 | }
30 | @keyframes stroke-draw {
31 | from {
32 | stroke: #de0f3f;
33 | stroke-dasharray: 1;
34 | }
35 | to {
36 | stroke: #aa1945;
37 | transform: rotate(180deg);
38 | stroke-dasharray: 8;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/utils/staticfiles.py:
--------------------------------------------------------------------------------
1 | from starlette.staticfiles import StaticFiles, PathLike
2 | from os import makedirs, path
3 | import typing
4 | from logging import error
5 |
6 |
7 | class CustomStaticFiles(StaticFiles):
8 | def __init__(
9 | self,
10 | *,
11 | directory: typing.Optional[PathLike] = None,
12 | packages: typing.Optional[
13 | typing.List[typing.Union[str, typing.Tuple[str, str]]]
14 | ] = None,
15 | html: bool = False,
16 | check_dir: bool = True,
17 | ) -> None:
18 |
19 | self.directory = directory
20 | self.packages = packages
21 | self.all_directories = self.get_directories(directory, packages)
22 | self.html = html
23 | self.config_checked = False
24 | if check_dir and directory is not None and not path.isdir(directory):
25 | error(f"Directory '{directory}' does not exist")
26 | makedirs(directory)
27 |
--------------------------------------------------------------------------------
/backend/errors/http_error.py:
--------------------------------------------------------------------------------
1 | from starlette.exceptions import HTTPException
2 | from starlette.responses import HTMLResponse
3 | from starlette.requests import Request
4 |
5 |
6 | async def bad_request_400(request: Request, exc: HTTPException = HTTPException(400), msg: str = "400 Bad Request"):
7 | return HTMLResponse(content=msg, status_code=exc.status_code)
8 |
9 |
10 | async def not_found_404(request: Request, exc: HTTPException = HTTPException(404), msg: str = "404 Not Found"):
11 | return HTMLResponse(content=msg, status_code=exc.status_code)
12 |
13 |
14 | async def internal_server_500(request: Request, exc: HTTPException = HTTPException(500), msg: str = "500 Internal Server"):
15 | return HTMLResponse(content=msg, status_code=exc.status_code)
16 |
17 |
18 | async def service_unavailable_503(request: Request, exc: HTTPException = HTTPException(503), msg: str = "Service Unavailable"):
19 | return HTMLResponse(content=msg, status_code=exc.status_code)
20 |
--------------------------------------------------------------------------------
/scripts/clean.js:
--------------------------------------------------------------------------------
1 | const { existsSync, readdirSync, rmdirSync, statSync, unlinkSync } = require("fs");
2 |
3 | /**
4 | * @namespace Cleaner
5 | * @description - Cleans project by removing several files & folders.
6 | * @see scripts\dispatch.js cleanProject() for complete list
7 | */
8 | class Cleaner {
9 | removePath = (pathToRemove) => {
10 | if (existsSync(pathToRemove)) {
11 | console.log(`Removing: ${pathToRemove}`);
12 |
13 | if (statSync(pathToRemove).isFile()) unlinkSync(pathToRemove);
14 | else {
15 | const files = readdirSync(pathToRemove);
16 |
17 | files.forEach((file) => {
18 | const filePath = `${pathToRemove}/${file}`;
19 |
20 | if (statSync(filePath).isDirectory()) this.removePath(filePath);
21 | else unlinkSync(filePath);
22 | });
23 | rmdirSync(pathToRemove);
24 | }
25 | }
26 | };
27 | }
28 |
29 | module.exports.Cleaner = Cleaner;
30 |
--------------------------------------------------------------------------------
/src/components/metadata-popup.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 |
3 | import {
4 | AlertDialog,
5 | AlertDialogBody,
6 | AlertDialogContent,
7 | AlertDialogHeader,
8 | AlertDialogOverlay,
9 | Progress,
10 | Text,
11 | } from "@chakra-ui/react";
12 |
13 | export default function MetaDataPopup({ onClose, onOpen, isOpen }) {
14 | const cancelRef = useRef();
15 | return (
16 | // @ts-ignore
17 |
23 |
24 |
25 |
26 | Starting download
27 |
28 |
29 | Loading meta for requested files. Please wait
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/backend/middleware/requestValidator.py:
--------------------------------------------------------------------------------
1 | from starlette.middleware.base import BaseHTTPMiddleware
2 | from starlette.requests import Request
3 | from starlette.responses import Response
4 | from json import JSONDecodeError
5 | from starlette.middleware.base import RequestResponseEndpoint
6 | from errors.http_error import bad_request_400
7 | from logging import info
8 |
9 |
10 | class RequestValidator(BaseHTTPMiddleware):
11 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
12 | try:
13 | if request.method == "POST":
14 | if request.headers.get("content-type", None) == "application/json":
15 | request.state.body = await request.json()
16 | return await call_next(request)
17 | except JSONDecodeError as msg:
18 | info(msg)
19 | return await bad_request_400(request, msg="Malformed body: Invalid JSON")
20 |
21 | except RuntimeError as exc:
22 | if str(exc) == "No response returned." and await request.is_disconnected():
23 | return Response(status_code=204)
24 | raise
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 @Cosmicoppai @NishantChaware @heyharshjaiswal
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect } from "react";
2 | import ReactDOM from "react-dom/client";
3 |
4 | import { ChakraProvider, useColorMode } from "@chakra-ui/react";
5 | import { Provider } from "react-redux";
6 |
7 | import "./styles/index.css";
8 |
9 | import App from "./App";
10 |
11 | import store from "./store/store";
12 |
13 | import { client, SocketContext } from "src/context/socket";
14 | import { theme } from "./styles/theme";
15 |
16 | function ForceDarkMode({ children }: {
17 | children: ReactNode
18 | }) {
19 | const { colorMode, toggleColorMode } = useColorMode();
20 |
21 | useEffect(() => {
22 | if (colorMode === "dark") return;
23 | toggleColorMode();
24 | }, [colorMode]);
25 |
26 | return <>{children}>;
27 | }
28 |
29 | ReactDOM
30 | .createRoot(document.getElementById("root"))
31 | .render(
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 |
--------------------------------------------------------------------------------
/src/components/network-error.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Heading, Text, Button } from "@chakra-ui/react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export default function NetworkError() {
5 |
6 | const navigate = useNavigate();
7 |
8 | return (
9 |
10 |
16 | 500
17 |
18 |
19 | No Internet Connection
20 |
21 |
22 | You dont seem to have an active internet connection. Please check your connection
23 | and try again
24 |
25 |
26 |
34 |
35 | );
36 | };
--------------------------------------------------------------------------------
/src/store/actions/downloadActions.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { createStandaloneToast } from "@chakra-ui/toast";
4 |
5 | import server from "src/utils/axios";
6 |
7 | import {
8 | ANIME_DOWNLOAD_FAIL,
9 | ANIME_DOWNLOAD_REQUEST,
10 | ANIME_DOWNLOAD_SUCCESS,
11 | } from "../constants/downloadConstants";
12 |
13 | const { toast } = createStandaloneToast();
14 | export const downloadVideo = (payload) => async (dispatch) => {
15 | try {
16 | dispatch({ type: ANIME_DOWNLOAD_REQUEST });
17 | const { data } = await server.post(`/download`, payload, {
18 | "Content-Type": "application/json",
19 | });
20 |
21 | console.log(data);
22 | dispatch({ type: ANIME_DOWNLOAD_SUCCESS, payload: data });
23 | toast({
24 | title: "Download has been started, Please check downloads section.",
25 | status: "success",
26 | duration: 2000,
27 | });
28 | } catch (error) {
29 | console.log(error);
30 | if (error?.response?.data) {
31 | toast({
32 | title: error?.response?.data,
33 | status: "error",
34 | duration: 2000,
35 | });
36 | }
37 |
38 | dispatch({ type: ANIME_DOWNLOAD_FAIL, payload: error });
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/LiSA.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 | from PyInstaller.config import CONF
4 | CONF['distpath'] = "./resources"
5 |
6 | block_cipher = None
7 |
8 |
9 | a = Analysis(
10 | ['backend\\LiSA.py'],
11 | pathex=[],
12 | binaries=[('ffmpeg.exe', '.')],
13 | datas=[('backend/defaults', 'defaults'), ('backend/sql_queries', 'sql_queries'),
14 | ('backend/config/config.json', '.')],
15 | hiddenimports=[],
16 | hookspath=[],
17 | hooksconfig={},
18 | runtime_hooks=[],
19 | excludes=[],
20 | win_no_prefer_redirects=False,
21 | win_private_assemblies=False,
22 | cipher=block_cipher,
23 | noarchive=False,
24 | )
25 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
26 |
27 | exe = EXE(
28 | pyz,
29 | a.scripts,
30 | [],
31 | exclude_binaries=True,
32 | name='LiSA',
33 | debug=False,
34 | bootloader_ignore_signals=False,
35 | strip=False,
36 | upx=True,
37 | console=False,
38 | disable_windowed_traceback=False,
39 | argv_emulation=False,
40 | target_arch=None,
41 | codesign_identity=None,
42 | entitlements_file=None,
43 | )
44 | coll = COLLECT(
45 | exe,
46 | a.binaries,
47 | a.zipfiles,
48 | a.datas,
49 | strip=False,
50 | upx=True,
51 | upx_exclude=[],
52 | name='lisa',
53 | )
54 |
--------------------------------------------------------------------------------
/src/styles/App.css:
--------------------------------------------------------------------------------
1 | .video-js .vjs-control-bar {
2 | color: #ffffff;
3 | font-size: 12px;
4 | }
5 |
6 | .video-js {
7 | font-size: 10px;
8 | color: #fff;
9 | }
10 |
11 | .vjs-default-skin .vjs-big-play-button {
12 | font-size: 3em;
13 |
14 | line-height: 2.5em;
15 | height: 2.5em;
16 | width: 2.5em;
17 | /* 0.06666em = 2px default */
18 | border: 0.06666em solid #fff;
19 | /* 0.3em = 9px default */
20 | border-radius: 50%;
21 | /* Align center */
22 | left: 50%;
23 | top: 50%;
24 | margin-left: -1.25em;
25 | margin-top: -1.25em;
26 | }
27 |
28 | .video-js .vjs-control-bar,
29 | .video-js .vjs-big-play-button,
30 | .video-js .vjs-menu-button .vjs-menu-content {
31 | background-color: #2b333f;
32 | background-color: rgba(43, 51, 63, 0);
33 | }
34 |
35 | .video-js .vjs-slider {
36 | background-color: #73859f;
37 | background-color: rgba(115, 133, 159, 0.5);
38 | }
39 |
40 | .video-js .vjs-volume-level,
41 | .video-js .vjs-play-progress,
42 | .video-js .vjs-slider-bar {
43 | background: #fff;
44 | }
45 |
46 | .video-js .vjs-load-progress {
47 | background: #bfc7d3;
48 | background: rgba(115, 133, 159, 0.5);
49 | }
50 |
51 | .video-js .vjs-load-progress div {
52 | background: rgb(0, 162, 255);
53 | background: rgb(0, 162, 255);
54 | }
55 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from "redux";
2 | import thunk from "redux-thunk";
3 | import { composeWithDevTools } from "redux-devtools-extension";
4 |
5 | import {
6 | animeCurrentEpReducer,
7 | animeDetailsReducer,
8 | animeEpisodesReducer,
9 | animeEpUrlReducer,
10 | animeExploreDetailsReducer,
11 | animeSearchListReducer,
12 | animeStreamDetailsReducer,
13 | animeStreamExternalReducer,
14 | recommendationsReducer,
15 | } from "./reducers/animeReducers";
16 |
17 | import { animeDownloadReducer, animeLibraryReducer } from "./reducers/downloadReducers";
18 |
19 | const reducer = combineReducers({
20 | animeSearchList: animeSearchListReducer,
21 | animeStreamDetails: animeStreamDetailsReducer,
22 | animeStreamExternal: animeStreamExternalReducer,
23 | animeDownloadDetails: animeDownloadReducer,
24 | animeLibraryDetails: animeLibraryReducer,
25 | animeEpisodesDetails: animeEpisodesReducer,
26 | animeCurrentEp: animeCurrentEpReducer,
27 | animeEpUrl: animeEpUrlReducer,
28 | animeExploreDetails: animeExploreDetailsReducer,
29 | animeDetails: animeDetailsReducer,
30 | animeRecommendations: recommendationsReducer,
31 | });
32 |
33 | const middleware = [thunk];
34 | const initialState = {};
35 |
36 | const store = createStore(
37 | reducer,
38 | initialState,
39 | composeWithDevTools(applyMiddleware(...middleware))
40 | );
41 |
42 | export default store;
43 |
--------------------------------------------------------------------------------
/src/components/back-button/back-button.css:
--------------------------------------------------------------------------------
1 | div.backBtn {
2 | width: 100px;
3 | left: 100px;
4 | top: 100px;
5 | background-color: #f4f4f4;
6 | transition: all 0.4s ease;
7 | position: fixed;
8 | cursor: pointer;
9 | }
10 |
11 | span.line {
12 | bottom: auto;
13 | right: auto;
14 | top: auto;
15 | left: auto;
16 | background-color: #333;
17 | border-radius: 10px;
18 | width: 100%;
19 | left: 0px;
20 | height: 2px;
21 | display: block;
22 | position: absolute;
23 | transition: width 0.2s ease 0.1s, left 0.2s ease, transform 0.2s ease 0.3s,
24 | background-color 0.2s ease;
25 | }
26 |
27 | span.tLine {
28 | top: 0px;
29 | }
30 |
31 | span.mLine {
32 | top: 13px;
33 | opacity: 0;
34 | }
35 |
36 | span.bLine {
37 | top: 26px;
38 | }
39 |
40 | .label {
41 | position: absolute;
42 | left: 0px;
43 | top: 5px;
44 | width: 100%;
45 | text-align: center;
46 | transition: all 0.4s ease;
47 | font-size: 1em;
48 | }
49 |
50 | div.backBtn:hover span.label {
51 | left: 25px;
52 | }
53 |
54 | div.backBtn:hover span.line {
55 | left: -10px;
56 | height: 5px;
57 | background-color: #ffff;
58 | }
59 |
60 | div.backBtn:hover span.tLine {
61 | width: 25px;
62 | transform: rotate(-45deg);
63 | left: -15px;
64 | top: 6px;
65 | }
66 |
67 | div.backBtn:hover span.mLine {
68 | opacity: 1;
69 | width: 30px;
70 | }
71 |
72 | div.backBtn:hover span.bLine {
73 | width: 25px;
74 | transform: rotate(45deg);
75 | left: -15px;
76 | top: 20px;
77 | }
78 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const { spawnSync } = require("child_process");
2 | const spawnOptions = { detached: false, shell: true, stdio: "inherit" };
3 |
4 | /**
5 | * @namespace Builder
6 | * @description - Builds React & Python builds of project so Electron can be used.
7 | */
8 | class Builder {
9 | /**
10 | * @description - Creates React and Python production builds.
11 | * @memberof Builder
12 | */
13 | buildAll = () => {
14 | const { buildPython, buildReact } = this;
15 |
16 | buildPython();
17 | buildReact();
18 | };
19 |
20 | /**
21 | * @description - Creates production build of Python back end.
22 | * @memberof Builder
23 | */
24 | buildPython = () => {
25 | console.log("Creating Python distribution files...");
26 |
27 | const app = "backend/LiSA.py";
28 | const icon = "./public/favicon.ico";
29 |
30 | const options = [
31 | "--noconfirm", // Don't confirm overwrite
32 | "--distpath ./resources", // Dist (out) path
33 | `--icon ${icon}`, // Icon to use
34 | ].join(" ");
35 | // TODO: Check if python is installed.. If not, prompt user
36 | // "Python is required but not installed, install it? (y/n)"
37 | spawnSync(`pyinstaller lisa.spec --clean`, spawnOptions);
38 | };
39 |
40 | /**
41 | * @description - Creates production build of React front end.
42 | * @memberof Builder
43 | */
44 | buildReact = () => {
45 | console.log("Creating React distribution files...");
46 | spawnSync(`react-scripts build`, spawnOptions);
47 | };
48 | }
49 |
50 | module.exports.Builder = Builder;
51 |
--------------------------------------------------------------------------------
/backend/video/streamer/stream.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import io
4 | from abc import ABC, abstractmethod
5 | from os import system
6 | from typing import Dict, List
7 | import subprocess
8 | import asyncio
9 | import shlex
10 |
11 |
12 | class Stream(ABC):
13 | players: Dict[str, Stream] = {}
14 |
15 | def __init_subclass__(cls, **kwargs):
16 | super().__init_subclass__(**kwargs)
17 | cls.players[cls._PLAYER_NAME] = cls
18 |
19 | @classmethod
20 | def play(cls, player_name: str, file_location: str):
21 | if player_name not in cls.players:
22 | raise ValueError("Bad player type {}".format(player_name))
23 | return cls.players[player_name].play_video(file_location)
24 |
25 | @classmethod
26 | @abstractmethod
27 | async def play_video(cls, file_location: str):
28 | ...
29 |
30 | @staticmethod
31 | async def _play_video(cmd: List[str]) -> Exception:
32 | video_player_process = await asyncio.create_subprocess_exec(*cmd)
33 | _, stderr = await video_player_process.communicate()
34 |
35 | if stderr:
36 | stderr = io.TextIOWrapper(io.BytesIO(stderr).read())
37 |
38 | if video_player_process.returncode != 0:
39 | raise subprocess.CalledProcessError(video_player_process.returncode, stderr)
40 |
41 |
42 | class MpvStream(Stream):
43 | _PLAYER_NAME: str = "mpv"
44 |
45 | @classmethod
46 | async def play_video(cls, file_location: str):
47 | await cls._play_video(shlex.split(f'mpv "{file_location}" --fs=yes --ontop'))
48 |
49 |
50 | class VlcStream(Stream):
51 | _PLAYER_NAME: str = "vlc"
52 |
53 | @classmethod
54 | async def play_video(cls, file_location: str):
55 | await cls._play_video(shlex.split(f'vlc "{file_location}" --fullscreen'))
56 |
57 |
58 |
--------------------------------------------------------------------------------
/backend/utils/init_db.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sqlite3
3 | from sys import exit as EXIT
4 | from config import DBConfig
5 | from typing import Dict, List
6 |
7 |
8 | class MetaDB(type):
9 | _instance = None
10 |
11 | def __call__(cls, *args, **kwargs):
12 | if not cls._instance:
13 | cls._instance = super().__call__(*args, **kwargs)
14 | return cls._instance
15 |
16 |
17 | class DB(metaclass=MetaDB):
18 | connection: sqlite3.Connection = sqlite3.connect(DBConfig.DB_PATH, check_same_thread=False)
19 | connection.row_factory = sqlite3.Row
20 | _highest_ids: Dict[str, int] = {}
21 |
22 | def __init__(self, table_names: List[str] = ("progress_tracker", )) -> int:
23 | _highestId: int = 1
24 | cur = self.connection.cursor()
25 | for table_name in table_names:
26 | _id = cur.execute(f"SELECT max(id) from {table_name}").fetchone()[0]
27 | if _id:
28 | _highestId = _id + 1
29 | DB._highest_ids[table_name] = _highestId
30 |
31 | @classmethod
32 | def migrate(cls, files: List[str] = ("progress_tracker.sql", "watchlist.sql")):
33 | cur = cls.connection.cursor()
34 | for fil in files:
35 | file_ = DBConfig.DEFAULT_SQL_DIR.joinpath(fil).__str__()
36 | with open(file_) as file:
37 | try:
38 | sql_queries = file.read()
39 | cur.executescript(sql_queries)
40 | cls.connection.commit()
41 | except sqlite3.Error as error:
42 | logging.error(error)
43 | EXIT()
44 | cur.close()
45 |
46 | @classmethod
47 | def get_id(cls, table_name: str = "progress_tracker") -> int:
48 | _id = cls._highest_ids[table_name]
49 | cls._highest_ids[table_name] = _id + 1 # update the highest id
50 | return _id
51 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | Jennie by Developer Rahul
25 |
26 |
27 |
28 |
29 |
39 |
40 |
--------------------------------------------------------------------------------
/src/store/reducers/downloadReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DOWNLOAD_LIBRARY_FAIL,
3 | DOWNLOAD_LIBRARY_REQUEST,
4 | DOWNLOAD_LIBRARY_SUCCESS,
5 | } from "../constants/animeConstants";
6 |
7 | import {
8 | ANIME_DOWNLOAD_FAIL,
9 | ANIME_DOWNLOAD_REQUEST,
10 | ANIME_DOWNLOAD_SUCCESS,
11 | } from "../constants/downloadConstants";
12 |
13 | export const animeDownloadReducer = (state = { loading: false, details: null }, action) => {
14 | switch (action.type) {
15 | case ANIME_DOWNLOAD_REQUEST:
16 | return {
17 | loading: true,
18 | details: null,
19 | };
20 |
21 | case ANIME_DOWNLOAD_SUCCESS:
22 | return { loading: false, details: action.payload };
23 |
24 | case ANIME_DOWNLOAD_FAIL:
25 | return { loading: false, error: action.payload };
26 |
27 | default:
28 | return state;
29 | }
30 | };
31 | export const animeLibraryReducer = (state = { loading: false, details: null }, action) => {
32 | switch (action.type) {
33 | case DOWNLOAD_LIBRARY_REQUEST:
34 | return {
35 | loading: true,
36 | details: null,
37 | };
38 |
39 | case DOWNLOAD_LIBRARY_SUCCESS:
40 | return { loading: false, details: action.payload };
41 |
42 | case DOWNLOAD_LIBRARY_FAIL:
43 | return { loading: false, error: action.payload };
44 |
45 | default:
46 | return state;
47 | }
48 | };
49 | export const liveDownloads = (state = { loading: false, details: null }, action) => {
50 | switch (action.type) {
51 | case DOWNLOAD_LIBRARY_REQUEST:
52 | return {
53 | loading: true,
54 | details: null,
55 | };
56 |
57 | case DOWNLOAD_LIBRARY_SUCCESS:
58 | return { loading: false, details: action.payload };
59 |
60 | case DOWNLOAD_LIBRARY_FAIL:
61 | return { loading: false, error: action.payload };
62 |
63 | default:
64 | return state;
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/backend/LiSA.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from video.downloader.msg_system import MsgSystem
3 | from threading import Thread
4 | from sys import stdout, argv
5 | import asyncio
6 | from utils import DB
7 | from config import ServerConfig, parse_config_json, update_environ, FileConfig
8 | from api import start_api_server
9 | from multiprocessing import Pipe, Manager, freeze_support
10 | from video.downloader import DownloadManager
11 | from video.library import Library
12 |
13 |
14 | def run_api_server(port: int = 8000):
15 | ServerConfig.API_SERVER_ADDRESS = f"http://localhost:{port}"
16 | print(f"server started on port: {port} \n You can access API SERVER on {ServerConfig.API_SERVER_ADDRESS}")
17 | start_api_server(port=port)
18 |
19 |
20 | def get_ports():
21 | _api_port, _web_sock_port = 6969, 9000
22 | try:
23 | if len(argv) >= 3:
24 | _api_port, _web_sock_port = int(argv[1]), int(argv[2])
25 | elif len(argv) >= 2:
26 | _api_port = int(argv[1])
27 | except ValueError:
28 | ...
29 | return _api_port, _web_sock_port
30 |
31 |
32 | if __name__ == "__main__":
33 | freeze_support()
34 | try:
35 | logging.basicConfig(stream=stdout, level=logging.ERROR)
36 | DB.migrate() # migrate the database
37 | DB() # initialize the highest id
38 |
39 | Library.data = Manager().dict() # update the dict into manager dict
40 | Library.load_datas() # load data of all tables in-mem
41 |
42 | """
43 | update configs and environment variables
44 | """
45 | parse_config_json(FileConfig.CONFIG_JSON_PATH)
46 | update_environ()
47 |
48 | api_port, web_sock_port = get_ports()
49 |
50 | t1 = Thread(target=run_api_server, args=(api_port,))
51 | t1.daemon = True
52 | t1.start()
53 |
54 | # initialize DownloadManager
55 |
56 | loop = asyncio.new_event_loop()
57 | asyncio.set_event_loop(loop)
58 |
59 | DownloadManager()
60 |
61 | MsgSystem.out_pipe, MsgSystem.in_pipe = Pipe()
62 | msg_system = MsgSystem(web_sock_port)
63 | loop.create_task(msg_system.send_updates())
64 |
65 | loop.run_until_complete(msg_system.run_server()) # run socket server
66 | except KeyboardInterrupt:
67 | DB.connection.close()
68 |
--------------------------------------------------------------------------------
/scripts/start.js:
--------------------------------------------------------------------------------
1 | const { spawn, spawnSync } = require("child_process");
2 | const { get } = require("axios");
3 |
4 | /**
5 | * @namespace Starter
6 | * @description - Scripts to start Electron, React, and Python.
7 | */
8 | class Starter {
9 | /**
10 | * @description - Starts developer mode.
11 | * @memberof Starter
12 | */
13 | developerMode = async () => {
14 | // Child spawn options for console
15 | const spawnOptions = {
16 | hideLogs: { detached: false, shell: true, stdio: "pipe" },
17 | showLogs: { detached: false, shell: true, stdio: "inherit" },
18 | };
19 |
20 | /**
21 | * Method to get first port in range of 3001-3999,
22 | * Remains unused here so will be the same as the
23 | * port used in main.js
24 | */
25 | // const port = await getPort({
26 | // port: getPort.makeRange(3001, 3999)
27 | // });
28 |
29 | // Kill anything that might using required React port
30 | // spawnSync('npx kill-port 3000', spawnOptions.hideLogs);
31 |
32 | // Start & identify React & Electron processes
33 | spawn("cross-env BROWSER=none react-scripts start", spawnOptions.showLogs);
34 | spawn("electron .", spawnOptions.showLogs);
35 |
36 | // Kill processes on exit
37 | const exitOnEvent = (event) => {
38 | process.once(event, () => {
39 | try {
40 | // These errors are expected since the connection is closing
41 | const expectedErrors = ["ECONNRESET", "ECONNREFUSED"];
42 |
43 | // Send command to Flask server to quit and close
44 | get(`http://localhost:3000/quit`).catch(
45 | (error) => !expectedErrors.includes(error.code) && console.log(error)
46 | );
47 | } catch (error) {
48 | // This errors is expected since the process is closing
49 | if (error.code !== "ESRCH") console.error(error);
50 | }
51 | });
52 | };
53 |
54 | // Set exit event handlers
55 | ["exit", "SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2", "uncaughtException"].forEach(
56 | exitOnEvent
57 | );
58 | };
59 | }
60 |
61 | module.exports = { Starter };
62 |
--------------------------------------------------------------------------------
/backend/scraper/base.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from abc import ABC
3 | import aiohttp
4 | from utils.headers import get_headers
5 | import logging
6 | from random import choice
7 | from typing import Tuple
8 | from multidict import CIMultiDictProxy
9 |
10 |
11 | class Scraper(ABC):
12 | session: aiohttp.ClientSession = None
13 | api_url: str = None
14 | content: bytes = None
15 |
16 | @classmethod
17 | async def set_session(cls):
18 | if not cls.session:
19 | cls.session = aiohttp.ClientSession()
20 |
21 | async def __aenter__(self):
22 | return self
23 |
24 | async def __aexit__(self, exc_type, exc_value, traceback):
25 | if self.session:
26 | await self.session.close()
27 |
28 | @classmethod
29 | async def get_api(cls, data: dict, headers: dict = get_headers()) -> dict:
30 | return await (await cls.get(cls.api_url, data, headers)).json()
31 |
32 | @classmethod
33 | async def get(cls, url: str, data=None, headers: dict = get_headers()) -> aiohttp.ClientResponse:
34 | if not cls.session:
35 | await Scraper.set_session()
36 |
37 | data = {} or data
38 | err, tries = None, 0
39 |
40 | while tries < 10:
41 | try:
42 | async with cls.session.get(url=url, params=data, headers=headers) as resp:
43 | if resp.status != 200:
44 | err = f"request failed with status: {resp.status}\n err msg: {resp.content}"
45 | logging.error(f"{err}\nRetrying...")
46 | raise aiohttp.ClientResponseError(None, None, message=err)
47 |
48 | cls.content = await resp.read() # read whole resp, before closing the connection
49 | return resp
50 | except (aiohttp.ClientOSError, asyncio.TimeoutError, aiohttp.ServerDisconnectedError, aiohttp.ServerTimeoutError):
51 | await asyncio.sleep(choice([5, 4, 3, 2, 1])) # randomly await
52 | tries += 1
53 | continue
54 |
55 | raise aiohttp.ClientResponseError(None, None, message=err)
56 |
57 |
58 | class Proxy(Scraper):
59 | @classmethod
60 | async def get(cls, url: str, data=None, headers=None) -> Tuple[bytes, CIMultiDictProxy[str]]:
61 | resp = await super().get(url, data, headers)
62 | return cls.content, resp.headers
63 |
--------------------------------------------------------------------------------
/src/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Link, useLocation } from "react-router-dom";
2 | import { Flex, Icon, Tooltip } from "@chakra-ui/react";
3 | import { AiOutlineCompass, AiOutlineDownload, AiOutlineSearch, AiOutlineSetting } from "react-icons/ai";
4 |
5 | export default function Navbar() {
6 |
7 | const { pathname } = useLocation();
8 |
9 | return (
10 |
19 |
20 |
21 | {" "}
22 |
29 |
30 |
31 |
32 |
33 |
34 | {" "}
35 |
42 |
43 |
44 |
45 |
46 |
47 | {" "}
48 |
55 |
56 |
57 |
58 | {/*
59 |
64 | */}
65 |
66 | );
67 | };
--------------------------------------------------------------------------------
/src/store/constants/animeConstants.ts:
--------------------------------------------------------------------------------
1 | export const ANIME_SEARCH_REQUEST = "ANIME_SEARCH_REQUEST";
2 | export const ANIME_SEARCH_SUCCESS = "ANIME_SEARCH_SUCCESS";
3 | export const ANIME_SEARCH_FAIL = "ANIME_SEARCH_FAIL";
4 | export const ANIME_SEARCH_CLEAR = "ANIME_SEARCH_CLEAR";
5 |
6 | export const ANIME_STREAM_EXTERNAL_REQUEST = "ANIME_STREAM_EXTERNAL_REQUEST";
7 | export const ANIME_STREAM_EXTERNAL_SUCCESS = "ANIME_STREAM_EXTERNAL_SUCCESS";
8 | export const ANIME_STREAM_EXTERNAL_FAIL = "ANIME_STREAM_EXTERNAL_FAIL";
9 | export const ANIME_STREAM_EXTERNAL_CLEAR = "ANIME_STREAM_EXTERNAL_CLEAR";
10 |
11 | export const ANIME_STREAM_URL_REQUEST = "ANIME_STREAM_URL_REQUEST";
12 | export const ANIME_STREAM_URL_SUCCESS = "ANIME_STREAM_URL_SUCCESS";
13 | export const ANIME_STREAM_URL_FAIL = "ANIME_STREAM_URL_FAIL";
14 | export const ANIME_STREAM_URL_CLEAR = "ANIME_STREAM_URL_CLEAR";
15 |
16 | export const ANIME_STREAM_DETAILS_REQUEST = "ANIME_STREAM_DETAILS_REQUEST";
17 | export const ANIME_STREAM_DETAILS_SUCCESS = "ANIME_STREAM_DETAILS_SUCCESS";
18 | export const ANIME_STREAM_DETAILS_FAIL = "ANIME_STREAM_DETAILS_FAIL";
19 | export const ANIME_STREAM_DETAILS_CLEAR = "ANIME_STREAM_DETAILS_CLEAR";
20 |
21 | export const ANIME_DETAILS_REQUEST = "ANIME_DETAILS_REQUEST";
22 | export const ANIME_DETAILS_SUCCESS = "ANIME_DETAILS_SUCCESS";
23 | export const ANIME_DETAILS_FAIL = "ANIME_DETAILS_FAIL";
24 |
25 | export const ANIME_EPISODES_ADD_REQUEST = "ANIME_EPISODES_ADD_REQUEST";
26 | export const ANIME_EPISODES_ADD_SUCCESS = "ANIME_EPISODES_ADD_SUCCESS";
27 | export const ANIME_EPISODES_ADD_FAIL = "ANIME_EPISODES_ADD_FAIL";
28 |
29 | export const ANIME_CURRENT_EP_REQUEST = "ANIME_CURRENT_EP_REQUEST";
30 | export const ANIME_CURRENT_EP_SUCCESS = "ANIME_CURRENT_EP_SUCCESS";
31 | export const ANIME_CURRENT_EP_FAIL = "ANIME_CURRENT_EP_FAIL";
32 |
33 | export const DOWNLOAD_LIBRARY_REQUEST = "DOWNLOAD_LIBRARY_REQUEST";
34 | export const DOWNLOAD_LIBRARY_SUCCESS = "DOWNLOAD_LIBRARY_SUCCESS";
35 | export const DOWNLOAD_LIBRARY_FAIL = "DOWNLOAD_LIBRARY_FAIL";
36 |
37 | export const ANIME_EXPLORE_DETAILS_REQUEST = "ANIME_EXPLORE_DETAILS_REQUEST";
38 | export const ANIME_EXPLORE_DETAILS_SUCCESS = "ANIME_EXPLORE_DETAILS_SUCCESS";
39 | export const ANIME_EXPLORE_DETAILS_FAIL = "ANIME_EXPLORE_DETAILS_FAIL";
40 |
41 | export const ANIME_RECOMMENDATION_REQUEST = "ANIME_RECOMMENDATION_REQUEST";
42 | export const ANIME_RECOMMENDATION_SUCCESS = "ANIME_RECOMMENDATION_SUCCESS";
43 | export const ANIME_RECOMMENDATION_FAIL = "ANIME_RECOMMENDATION_FAIL";
44 |
--------------------------------------------------------------------------------
/backend/video/downloader/msg_system.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import websockets
3 | import json
4 | from websockets.legacy.server import WebSocketServerProtocol
5 | from websockets.exceptions import ConnectionClosed
6 | from json import JSONDecodeError
7 | from config import ServerConfig
8 | from multiprocessing.connection import Connection
9 | from typing import Any, Dict
10 | from video.library import DBLibrary
11 |
12 |
13 | class MsgSystemMeta(type):
14 | _instance = None
15 |
16 | def __call__(cls, *args, **kwargs):
17 | if not cls._instance:
18 | cls._instance = super().__call__(*args, **kwargs)
19 | return cls._instance
20 |
21 |
22 | class MsgSystem(metaclass=MsgSystemMeta):
23 | connected_client: WebSocketServerProtocol = None
24 | _instance = None
25 | out_pipe: Connection = None
26 | in_pipe: Connection = None
27 |
28 | def __init__(self, port: int = 9000):
29 | ServerConfig.SOCKET_SERVER_ADDRESS = f"ws://localhost:{port}"
30 | self.ws_port = port
31 |
32 | async def run_server(self):
33 | async with websockets.serve(MsgSystem._server_handler, "", self.ws_port):
34 | print(f"Socket server started on port: {self.ws_port}\n You can access SOCKET SERVER on {ServerConfig.SOCKET_SERVER_ADDRESS}")
35 | await asyncio.Future() # run forever
36 | self.in_pipe.send(None) # cancel send_updates task
37 | self.in_pipe.close()
38 |
39 | @classmethod
40 | async def _server_handler(cls, websocket: websockets):
41 | try:
42 | async for msg in websocket:
43 | event = json.loads(msg)
44 | if event.get("type", "") == "connect" and not cls.connected_client:
45 | cls.connected_client = websocket
46 | print(f"connected with {websocket}")
47 | cls.connected_client = None
48 | except ConnectionClosed:
49 | cls.connected_client = None
50 | except JSONDecodeError:
51 | await websocket.send("Invalid connection request, pass valid JSON")
52 | await websocket.close(code=1000, reason="Invalid JSON")
53 |
54 | @classmethod
55 | async def send_updates(cls):
56 | while True:
57 | await asyncio.sleep(0.25)
58 | if cls.out_pipe.poll(): # poll for msg
59 | msg: Dict[str, Any] = cls.out_pipe.recv()
60 | if not msg:
61 | break
62 | if cls.connected_client: # send msg if any client is connected
63 | await cls.connected_client.send(json.dumps(msg))
64 |
--------------------------------------------------------------------------------
/backend/config/__init__.py:
--------------------------------------------------------------------------------
1 | from os import environ
2 | import json
3 | from pathlib import Path
4 | import logging as logger
5 | from platform import system
6 | from typing import Dict
7 | from dataclasses import dataclass
8 |
9 | "----------------------------------------------------------------------------------------------------------------------------------"
10 | # Server Configurations
11 |
12 |
13 | @dataclass
14 | class ServerConfig:
15 |
16 | API_SERVER_ADDRESS: str
17 | SOCKET_SERVER_ADDRESS: str
18 |
19 |
20 | "----------------------------------------------------------------------------------------------------------------------------------"
21 |
22 | "----------------------------------------------------------------------------------------------------------------------------------"
23 |
24 | # Default directories and file locations
25 |
26 |
27 | @dataclass
28 | class FileConfig:
29 |
30 | DEFAULT_DIR: Path = Path(__file__).resolve().parent.parent.joinpath("defaults/")
31 |
32 | CONFIG_JSON_PATH: Path = Path(__file__).resolve().parent.joinpath("config.json")
33 |
34 | DEFAULT_DOWNLOAD_LOCATION: Path = Path(__file__).resolve().parent.parent.parent.joinpath("downloads")
35 |
36 |
37 | "----------------------------------------------------------------------------------------------------------------------------------"
38 |
39 |
40 | "----------------------------------------------------------------------------------------------------------------------------------"
41 | # Database Configuration
42 |
43 |
44 | @dataclass(frozen=True)
45 | class DBConfig:
46 |
47 | DEFAULT_SQL_DIR: Path = Path(__file__).parent.parent.joinpath("sql_queries")
48 |
49 | DB_PATH: str = str(Path(__file__).parent.parent.joinpath("lisa")) # database path
50 |
51 |
52 | "----------------------------------------------------------------------------------------------------------------------------------"
53 |
54 | # ffmpeg extensions
55 |
56 | _ffmpeg_exts: Dict[str, str] = {"windows": "ffmpeg.exe", "linux": "ffmpeg", "darwin": "ffmpeg"}
57 |
58 |
59 | def parse_config_json(file_path: str | Path):
60 | try:
61 | with open(file_path, "r") as config_file:
62 | data = json.load(config_file)
63 | if data.get("download_location", None):
64 | FileConfig.DEFAULT_DOWNLOAD_LOCATION = Path(data["download_location"])
65 | except FileNotFoundError:
66 | ...
67 | except PermissionError as e:
68 | logger.error(e)
69 |
70 |
71 | def update_environ():
72 | ffmpeg_path: Path = Path(__file__).resolve().parent.parent.joinpath(_ffmpeg_exts[system().lower()])
73 | if Path(ffmpeg_path).exists():
74 | environ["ffmpeg"] = str(ffmpeg_path)
75 |
--------------------------------------------------------------------------------
/src/components/externalPopup.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {
4 | Box,
5 | Flex,
6 | Image,
7 | Modal,
8 | ModalBody,
9 | ModalCloseButton,
10 | ModalContent,
11 | ModalHeader,
12 | ModalOverlay,
13 | Progress,
14 | Text,
15 | } from "@chakra-ui/react";
16 |
17 | import { useDispatch, useSelector } from "react-redux";
18 | import { playVideoExternal } from "../store/actions/animeActions";
19 |
20 | import mpvImg from 'src/assets/img/mpv.png';
21 | import vlcImg from 'src/assets/img/vlc.png';
22 |
23 | export default function ExternalPlayerPopup({ isOpen, onOpen, onClose, language, historyPlay, playId }) {
24 |
25 | const dispatch = useDispatch();
26 |
27 | const { error: externalError, loading: externalLoading } = useSelector(
28 | (state) => state.animeStreamExternal
29 | );
30 | const { details } = useSelector((state) => state.animeStreamDetails);
31 | const playHandler = async (player) => {
32 | try {
33 | if (historyPlay) {
34 | await dispatch(
35 | playVideoExternal({
36 | id: playId,
37 | player,
38 | })
39 | );
40 | onClose();
41 | } else {
42 | if (details) {
43 | await dispatch(
44 | playVideoExternal({
45 | manifest_url: details[language],
46 | player,
47 | })
48 | );
49 | onClose();
50 | }
51 | }
52 | } catch (error) {
53 | console.log(error);
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
61 | Choose your favourite video player
62 |
63 |
64 |
65 | playHandler("mpv")} sx={{ cursor: "pointer" }}>
66 |
67 |
68 | playHandler("vlc")}>
69 |
70 |
71 |
72 | {externalError && (
73 |
74 | {externalError ? externalError["error"] : ""}
75 |
76 | )}
77 | {externalLoading && (
78 |
79 | {" "}
80 |
81 |
82 | Loading video in your local player, Please wait..
83 |
84 |
85 | )}
86 |
87 |
88 |
89 | );
90 | };
--------------------------------------------------------------------------------
/src/components/downloadItem.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Progress, Td, Text, Tr } from "@chakra-ui/react";
2 | import { AiOutlineClose, AiOutlinePause } from "react-icons/ai";
3 | import { FaPlay } from "react-icons/fa";
4 | import { formatBytes } from "../utils/formatBytes";
5 |
6 | export default function DownloadItem({
7 | key,
8 | data,
9 | cancelDownloadHandler,
10 | pauseDownloadHandler,
11 | resumeDownloadHandler,
12 | }) {
13 | return (
14 |
15 | |
16 | {" "}
17 | {data.status === "paused" && (
18 |
19 | resumeDownloadHandler(data.id)}
23 | />
24 |
25 | )}
26 | {(data.status === "started" || data.status === "scheduled") && (
27 |
28 | pauseDownloadHandler(data.id)}
32 | />
33 |
34 | )}
35 | |
36 |
37 |
38 | {data.file_name}
39 |
40 | |
41 |
42 | {" "}
43 | {data.status === "started" ? (
44 |
49 | ) : (
50 | data.status
51 | )}
52 | |
53 |
54 | {" "}
55 | {data.status === "started" && (
56 |
57 | {data.speed ? `${formatBytes(data.speed)}/ sec` : "--"}
58 |
59 | )}
60 | |
61 |
62 | {" "}
63 | {data.status === "started" ? (
64 |
65 | {!data.downloaded
66 | ? `-- / ${formatBytes(data.total_size)}`
67 | : `${formatBytes(data.downloaded)} / ${formatBytes(data.total_size)}`}
68 |
69 | ) : (
70 |
71 | {formatBytes(data.total_size)}
72 |
73 | )}
74 | |
75 |
76 | {" "}
77 |
78 | cancelDownloadHandler(data.id)}
82 | />
83 |
84 | |
85 |
86 | );
87 | };
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from "react";
2 | import { HashRouter, Routes, Route } from "react-router-dom";
3 | import { Box, Grid, GridItem } from "@chakra-ui/react";
4 |
5 | import Navbar from "./components/navbar";
6 |
7 | import { useSocketContext } from "./context/socket";
8 | import useSocketStatus from "./hooks/useSocketStatus";
9 |
10 | import AnimeDetailsScreen from "./screens/animeDetailsScreen";
11 | import SettingScreen from "./screens/settingScreen";
12 | import { HomeScreen } from "./screens/homeScreen";
13 | import DownloadScreen from "./screens/downloadsScreen";
14 | import InbuiltPlayerScreen from "./screens/inbuiltPlayerScreen";
15 | import ExploreScreen from "./screens/exploreScreen";
16 |
17 | import './styles/App.css';
18 |
19 | export default function App() {
20 |
21 | const { isSocketConnected } = useSocketStatus();
22 |
23 | const client = useSocketContext();
24 |
25 | console.log("client", client);
26 |
27 | useEffect(() => {
28 |
29 | if (!client) return;
30 | else if (client.readyState === 1) {
31 | client.send(JSON.stringify({ type: "connect" }));
32 | console.log("WebSocket Client Connected");
33 |
34 | // client.onopen = () => {
35 | // console.log("WebSocket Client Connected");
36 | // client.send(JSON.stringify({ type: "connect" }));
37 | // };
38 | }
39 |
40 | () => client.close();
41 |
42 | }, [isSocketConnected, client]);
43 |
44 | return (
45 |
46 |
47 |
56 |
57 |
58 |
73 |
74 |
75 | } />
76 | } />
77 | } />
78 | } />
79 | } />
80 | } />
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lisa",
3 | "version": "1.0.0",
4 | "author": "",
5 | "description": "",
6 | "private": true,
7 | "homepage": "./",
8 | "main": "main.js",
9 | "license": "MIT",
10 | "browserslist": {
11 | "production": [
12 | ">0.2%",
13 | "not dead",
14 | "not op_mini all"
15 | ],
16 | "development": [
17 | "last 1 chrome version",
18 | "last 1 firefox version",
19 | "last 1 safari version"
20 | ]
21 | },
22 | "scripts": {
23 | "build": "node ./scripts/dispatch build all",
24 | "build:all": "node ./scripts/dispatch build all",
25 | "build:react": "node ./scripts/dispatch build react",
26 | "build:python": "node ./scripts/dispatch build python",
27 | "build:package:linux": "node ./scripts/dispatch package linux",
28 | "build:package:mac": "node ./scripts/dispatch package mac",
29 | "build:package:windows": "node ./scripts/dispatch package windows",
30 | "clean": "node ./scripts/dispatch clean",
31 | "eject": "react-scripts eject",
32 | "start": "node ./scripts/dispatch start",
33 | "start:electron": "electron .",
34 | "start:react": "react-scripts start",
35 | "test": "react-scripts test"
36 | },
37 | "build": {
38 | "appId": "com.lisa.desktop",
39 | "extraResources": [
40 | "./resources/app"
41 | ]
42 | },
43 | "optionalDependencies": {
44 | "electron-installer-debian": "^3.1.0"
45 | },
46 | "devDependencies": {
47 | "@testing-library/jest-dom": "^4.2.4",
48 | "@testing-library/react": "^9.3.2",
49 | "@testing-library/user-event": "^7.1.2",
50 | "concurrently": "^5.3.0",
51 | "cross-env": "^7.0.3",
52 | "electron": "^13.0.1",
53 | "electron-builder": "^22.9.1",
54 | "electron-devtools-installer": "^3.2.0",
55 | "electron-installer-dmg": "^3.0.0",
56 | "electron-packager": "^15.0.0",
57 | "electron-reload": "^2.0.0-alpha.1",
58 | "electron-wix-msi": "^3.0.4",
59 | "eslint-config-airbnb": "^18.2.1",
60 | "eslint-plugin-import": "^2.23.4",
61 | "eslint-plugin-jsx-a11y": "^6.4.1",
62 | "eslint-plugin-react": "^7.24.0",
63 | "eslint-plugin-standard": "^5.0.0",
64 | "jsdoc": "^3.6.5",
65 | "prettier": "^2.2.1",
66 | "sass": "^1.26.5",
67 | "typescript": "^4.9.5",
68 | "wait-on": "^5.2.1"
69 | },
70 | "dependencies": {
71 | "@babel/eslint-parser": "^7.19.1",
72 | "@chakra-ui/icons": "^2.0.4",
73 | "@chakra-ui/react": "^2.2.4",
74 | "@electron/remote": "^1.0.2",
75 | "@emotion/react": "^11.10.0",
76 | "@emotion/styled": "^11.10.0",
77 | "@fontsource/montserrat": "^4.5.13",
78 | "@reduxjs/toolkit": "^1.1.0",
79 | "axios": "^0.27.2",
80 | "electron-is-dev": "^1.2.0",
81 | "framer-motion": "4.1.17",
82 | "get-port": "^5.1.1",
83 | "i": "^0.3.7",
84 | "npm": "^9.1.2",
85 | "prop-types": "^15.7.2",
86 | "ps-tree": "^1.2.0",
87 | "react": "18.2.0",
88 | "react-dom": "^18.2.0",
89 | "react-icons": "^4.4.0",
90 | "react-redux": "^8.0.2",
91 | "react-router-dom": "^6.3.0",
92 | "react-scripts": "^5.0.1",
93 | "redux": "^4.2.0",
94 | "redux-devtools-extension": "^2.13.9",
95 | "redux-thunk": "^2.4.1",
96 | "video.js": "^7.20.2",
97 | "videojs-contrib-quality-levels": "^2.1.0",
98 | "videojs-hls-quality-selector": "^1.1.4",
99 | "videojs-hotkeys": "^0.2.27",
100 | "web-vitals": "^0.2.4",
101 | "websocket": "^1.0.34"
102 | }
103 | }
--------------------------------------------------------------------------------
/src/screens/exploreScreen.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { Center, Flex, Heading, Skeleton, Spacer, Stack } from "@chakra-ui/react";
4 | import { useEffect, useState } from "react";
5 | import { Select } from "@chakra-ui/react";
6 | import { AiFillFilter } from "react-icons/ai";
7 | import { useDispatch, useSelector } from "react-redux";
8 |
9 | import { getExploreDetails } from "../store/actions/animeActions";
10 | import Card from "../components/card";
11 |
12 | export default function ExploreScreen() {
13 |
14 | const [query, setQuery] = useState("airing");
15 |
16 | const { loading, details } = useSelector((state) => {
17 | return state.animeExploreDetails;
18 | });
19 |
20 | const filterChangeHandler = (e) => {
21 | setQuery(e.target.value);
22 | };
23 |
24 | const dispatch = useDispatch();
25 |
26 | useEffect(() => {
27 | if (window) {
28 | window?.scrollTo(0, 0);
29 | }
30 | }, []);
31 |
32 | useEffect(() => {
33 | dispatch(getExploreDetails(query));
34 | }, [query]);
35 |
36 | return (
37 |
38 |
46 |
47 |
48 | Explore
49 |
50 |
51 | }
55 | value={query}>
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
79 | {details
80 | ? details?.data?.map((anime, index) => {
81 | return ;
82 | })
83 | : Array(30)
84 | .fill(0)
85 | .map(() => (
86 |
92 | ))
93 | }
94 |
95 |
96 |
97 |
98 |
99 | );
100 | };
--------------------------------------------------------------------------------
/src/components/ep-popover.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { useState } from "react";
4 | import {
5 | Button,
6 | Modal,
7 | ModalBody,
8 | ModalCloseButton,
9 | ModalContent,
10 | ModalFooter,
11 | ModalHeader,
12 | ModalOverlay,
13 | Select,
14 | Stack,
15 | } from "@chakra-ui/react";
16 |
17 | import { useSelector, useDispatch } from "react-redux";
18 |
19 | import { playVideoExternal } from "../store/actions/animeActions";
20 | import { downloadVideo } from "../store/actions/downloadActions";
21 |
22 | export default function EpPopover({ isOpen, onOpen, onClose }) {
23 |
24 | const dispatch = useDispatch();
25 | const { details, loading } = useSelector((state) => state.animeStreamDetails);
26 | const [language, setLanguage] = useState(null);
27 | const [quality, setQuality] = useState(null);
28 |
29 | const playHandler = () => {
30 | if (Object.values(Object.values(details[language])[0])[0]) {
31 | dispatch(playVideoExternal(Object.values(Object.values(details[language])[0])[0]));
32 | }
33 | };
34 | const downloadHandler = () => {
35 | if (Object.values(Object.values(details[language])[0])[0]) {
36 | dispatch(downloadVideo(Object.values(Object.values(details[language])[0])[0]));
37 | }
38 | };
39 | return (
40 | <>
41 | {details && (
42 |
43 |
44 |
45 | Select quality and action
46 |
47 |
48 |
49 |
61 |
74 |
75 |
76 |
77 |
78 |
81 |
84 |
87 |
88 |
89 |
90 | )}
91 | >
92 | );
93 | };
--------------------------------------------------------------------------------
/scripts/dispatch.js:
--------------------------------------------------------------------------------
1 | const [, , script, command] = process.argv;
2 | const { existsSync, readdirSync } = require("fs");
3 | const path = require("path");
4 |
5 | const { Builder } = require("./build");
6 | const { Cleaner } = require("./clean");
7 | const { Packager } = require("./package");
8 | const { Starter } = require("./start");
9 |
10 | /**
11 | * @namespace Dispatcher
12 | * @description - Dispatches script commands to various scripts.
13 | * @argument script - Script manager to use (e.g., build or package).
14 | * @argument command - Command argument describing exact script to run.
15 | */
16 |
17 | switch (script) {
18 | case "build":
19 | return buildApp();
20 |
21 | case "clean":
22 | return cleanProject();
23 |
24 | case "package":
25 | return packageApp();
26 |
27 | case "start":
28 | return startDeveloperMode();
29 |
30 | // no default
31 | }
32 |
33 | /**
34 | * @description - Builds various production builds (e.g., Python, React).
35 | * @memberof Dispatcher
36 | */
37 | function buildApp() {
38 | const builder = new Builder();
39 |
40 | switch (command) {
41 | case "react":
42 | return builder.buildReact();
43 |
44 | case "python":
45 | return builder.buildPython();
46 |
47 | case "all":
48 | return builder.buildAll();
49 |
50 | // no default
51 | }
52 | }
53 |
54 | /**
55 | * @description - Cleans project by removing various files and folders.
56 | * @memberof Dispatcher
57 | */
58 | function cleanProject() {
59 | const cleaner = new Cleaner();
60 | const getPath = (...filePaths) => path.join(__dirname, "..", ...filePaths);
61 |
62 | // Files to remove during cleaning
63 | [
64 | // Cache
65 | getPath("app.pyc"),
66 | getPath("app.spec"),
67 | getPath("__pycache__"),
68 |
69 | // Debug
70 | getPath("npm-debug.log"),
71 | getPath("yarn-debug.log"),
72 | getPath("yarn-error.log"),
73 |
74 | // Dependencies
75 | getPath(".pnp"),
76 | getPath(".pnp.js"),
77 | getPath("node_modules"),
78 | getPath("package-lock.json"),
79 | getPath("yarn.lock"),
80 |
81 | // Testing
82 | getPath("coverage"),
83 |
84 | // Production
85 | getPath("build"),
86 | getPath("dist"),
87 | getPath("docs"),
88 |
89 | // Misc
90 | getPath(".DS_Store"),
91 | ]
92 | // Iterate and remove process
93 | .forEach(cleaner.removePath);
94 |
95 | /**
96 | * Remove resources/app if it exists, then if the resources
97 | * folder isn't used for any other Python modules, delete it too.
98 | */
99 | const resourcesDir = getPath("resources");
100 | const isResourcesDirExist = existsSync(resourcesDir);
101 |
102 | if (isResourcesDirExist) {
103 | // Remove 'resources/app' directory if it exists
104 | const resourcesAppDir = path.join(resourcesDir, "app");
105 | const isResourcesAppDir = existsSync(resourcesAppDir);
106 |
107 | if (isResourcesAppDir) cleaner.removePath(resourcesAppDir);
108 |
109 | // Remove 'resources' directory if it's empty
110 | const isResourcesDirEmpty = Boolean(!readdirSync(resourcesDir).length);
111 | if (isResourcesDirEmpty) cleaner.removePath(resourcesDir);
112 | }
113 |
114 | console.log("Project is clean.");
115 | }
116 |
117 | /**
118 | * @description - Builds various installers (e.g., DMG, MSI).
119 | * @memberof Dispatcher
120 | */
121 | function packageApp() {
122 | const packager = new Packager();
123 |
124 | switch (command) {
125 | case "linux":
126 | return packager.packageLinux();
127 |
128 | case "mac":
129 | return packager.packageMacOS();
130 |
131 | case "windows":
132 | return packager.packageWindows();
133 |
134 | // no default
135 | }
136 | }
137 |
138 | /**
139 | * @description - Starts developer mode of app.
140 | * Including; React, Electron, and Python/Flask.
141 | * @memberof Dispatcher
142 | */
143 | function startDeveloperMode() {
144 | const start = new Starter();
145 | start.developerMode();
146 | }
147 |
--------------------------------------------------------------------------------
/backend/video/library/library.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | This file will handle the saving and extraction of metadata about downloaded files.
4 |
5 | """
6 | from __future__ import annotations
7 |
8 | from abc import ABC, abstractmethod
9 | from typing import Dict, List, Any
10 | import json
11 | from pathlib import Path
12 | from utils import DB
13 | from sqlite3 import IntegrityError
14 |
15 |
16 | class Library(ABC):
17 | data: Dict[int, Dict[str, Any]] = {}
18 | _libraries: List[Library] = []
19 | table_name: str
20 | fields: str = ""
21 | oid: str = "id"
22 |
23 | def __init_subclass__(cls, **kwargs):
24 | super().__init_subclass__(**kwargs)
25 | cls._libraries.append(cls)
26 |
27 | @classmethod
28 | def load_datas(cls):
29 | for lib in cls._libraries:
30 | lib.load_data()
31 |
32 | @classmethod
33 | def update(cls, _id: int, data: Dict[str, Any]) -> None:
34 | set_statement, field_values = cls.__query_builder(data, "update")
35 | field_values.append(_id)
36 | cmd = f"UPDATE {cls.table_name} SET {set_statement} WHERE {cls.oid}=?"
37 | cur = DB.connection.cursor()
38 | cur.execute(cmd, field_values)
39 | DB.connection.commit()
40 | cur.execute(f"SELECT {cls.fields} from {cls.table_name} WHERE {cls.oid}=?;", [_id, ])
41 | cls.data[_id] = dict(cur.fetchone())
42 | cur.close()
43 |
44 | @classmethod
45 | def load_data(cls) -> None:
46 |
47 | """
48 | Load all data from database
49 | """
50 | cur = DB.connection.cursor()
51 | cur.execute(f"SELECT {cls.fields} from {cls.table_name};")
52 | for row in cur.fetchall():
53 | data = dict(row)
54 | cls.data[data[cls.oid]] = data
55 | cur.close()
56 |
57 | @classmethod
58 | def get_all(cls) -> List[Dict[int, Dict[str, Any]]]:
59 | return [data for data in cls.data.values()]
60 |
61 | @classmethod
62 | def get(cls, filters: Dict[str, Any], query: List[str] = ("*",)) -> List[Dict[str, Any]]:
63 | cur = DB.connection.cursor()
64 |
65 | _query: str = ""
66 | for idx, _queri in enumerate(query):
67 | if idx != 0:
68 | _query += ","
69 | _query += _queri
70 |
71 | cmd = f"SELECT {_query} FROM {cls.table_name} WHERE "
72 | for idx, _filter in enumerate(filters):
73 | if idx != 0:
74 | cmd += "AND "
75 | cmd += f"{_filter}='{filters[_filter]}'"
76 |
77 | cur.execute(cmd)
78 | data = [dict(row) for row in cur.fetchall()]
79 | cur.close()
80 | return data
81 |
82 | @classmethod
83 | def create(cls, data: Dict[str, Any]) -> None:
84 | set_statement, field_values = cls.__query_builder(data)
85 | cmd = f"INSERT INTO {cls.table_name} ({set_statement}) VALUES {'(' + ','.join('?' * len(data)) + ')'}"
86 | try:
87 | cur = DB.connection.cursor()
88 | cur.execute(cmd, field_values)
89 | DB.connection.commit()
90 | cur.close()
91 | except IntegrityError:
92 | raise ValueError("Record already exist")
93 |
94 | cls.data[data["id"]] = data
95 |
96 | @classmethod
97 | def delete(cls, _id: int) -> None:
98 | del cls.data[_id]
99 | cur = DB.connection.cursor()
100 | cur.execute(f"DELETE FROM {cls.table_name} WHERE {cls.oid}=?", [_id, ])
101 | DB.connection.commit()
102 | cur.close()
103 |
104 | @staticmethod
105 | def __query_builder(data: Dict[str, Any], typ: str = "insert") -> (str, list):
106 | fields_to_set = []
107 | field_values = []
108 | for key in data:
109 | if typ == "insert":
110 | fields_to_set.append(key)
111 | else:
112 | fields_to_set.append(key + "=?")
113 | field_values.append(data[key])
114 | set_statement = ", ".join(fields_to_set)
115 | return set_statement, field_values
116 |
117 |
118 | class DBLibrary(Library):
119 | table_name: str = "progress_tracker"
120 | fields: str = "id, type, series_name, file_name, status, created_on, total_size, file_location"
121 | oid: str = "id"
122 |
123 |
124 | class WatchList(Library):
125 | table_name: str = "watchlist"
126 | fields: str = "anime_id, jp_name, no_of_episodes, type, status, season, year, score, poster, ep_details, created_on"
127 | oid: str = "anime_id"
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | 
6 | [](https://github.com/Cosmicoppai/LiSA/network)
7 | [](https://github.com/Cosmicoppai/LiSA/stargazers)
8 | [](https://github.com/Cosmicoppai/LiSA/issues)
9 | [](./LICENSE)
10 |
11 | 
12 | 
13 | 
14 |
15 |
16 |
17 | # Jennie - Anime player
18 |
19 | > A Desktop application, for streaming and downloading your favourite anime.
20 |
21 | ## CONTENTS OF THE FILE
22 |
23 | - [Features](#-features)
24 | - [Dependencies](#dependencies)
25 | - [Download](#-download)
26 | - [Installation](#-installation)
27 | - [Demo](#-demo)
28 | - [Future Plans](#future-plans)
29 | - [FAQ](#-faq)
30 | - Appendix
31 | - [Supported Webistes](#%EF%B8%8F-supported-websites)
32 | - [Supported External Video Player](#-supported-external-players)
33 | - [Filters](#filters)
34 | - [Contributing](#-contributing)
35 | - [Support](#-support)
36 | - [License](#-license)
37 | - [Disclaimer](#disclaimer)
38 |
39 | ## 🚀 Features
40 |
41 | - A User Friendly Interface
42 | - Download anime from [supported websites](#-supported-websites) in multiple resolutions and languages
43 | - Batch Download
44 | - Stream anime on the inbuilt player and your favourite [external video player](#-supported-external-players)
45 | - Explore anime based on different [filters](#filters)
46 | - Download Manager
47 | - Library to view pre-downloaded episodes and active downloads
48 | - Recommendation System
49 |
50 |
51 |
52 | ## Dependencies
53 |
54 | - [ffmpeg](https://ffmpeg.org/download.html)
55 |
56 |
57 |
58 |
59 | ## 📖 Installation
60 |
61 | ### Building From Source
62 |
63 | - Clone the project using
64 |
65 | ```cli
66 | git clone https://github.com/Cosmicoppai/LiSA.git
67 | ```
68 |
69 | #### Prerequisites
70 |
71 | - Make sure python 3.10 and node 18 are installed.
72 |
73 | #### Installing
74 |
75 | 1. Create and activate the virtual environment
76 |
77 | ```cli
78 | python -m venv ./env
79 | env/Script/activate
80 | ```
81 |
82 | 2. Install the dependencies using the `requirements.txt` and `build_requirements.txt` files.
83 |
84 | ```cli
85 | pip install -r ./requirements.txt
86 | pip install -r ./build_requirements.txt
87 | ```
88 |
89 | 3. Create `.env` & paste the following content
90 |
91 | ```dotenv
92 | REACT_APP_SERVER_URL=http://localhost:6969
93 | REACT_APP_SOCKET_URL=ws://localhost:9000
94 | ```
95 |
96 | 4. Install Node modules
97 |
98 | ```
99 | npm install
100 | ```
101 |
102 | 5. Add `ffmpeg` executable to `root folder` or in `PATH` Var.
103 |
104 | 6. run package using
105 |
106 | ```
107 | npm start
108 | ```
109 |
110 | 7. Build package using
111 |
112 | ```cli
113 | npm run build:package:windows
114 | ```
115 |
116 | Note:
117 |
118 | > Make sure to allow app to run as admin and allow incomming port forwarding on (`6969`, `9000`).
119 |
120 |
121 |
122 | ### Environment Tested on
123 |
124 | - Tested on Windows 8, 10 & 11.
125 |
126 |
127 |
128 | ## 😁 Demo
129 |
130 | 
131 | 
132 | 
133 | 
134 |
135 |
136 |
137 |
138 | ## DISCLAIMER
139 |
140 | This software has been developed just to improve users experience while streaming and downloading anime. Please support original content creators!
141 |
--------------------------------------------------------------------------------
/scripts/package.js:
--------------------------------------------------------------------------------
1 | const { spawnSync } = require("child_process");
2 | const { Builder } = require("./build");
3 |
4 | const builder = new Builder();
5 |
6 | // Define input and output directories
7 | const path = (directory) => {
8 | return require("path").resolve(__dirname, directory);
9 | };
10 |
11 | /**
12 | * @namespace Packager
13 | * @description - Packages app for various operating systems.
14 | */
15 | class Packager {
16 | /**
17 | * @description - Creates DEB installer for linux.
18 | * @memberof Packager
19 | *
20 | * @tutorial https://github.com/electron-userland/electron-installer-debian
21 | */
22 | packageLinux = () => {
23 | // Build Python & React distribution files
24 | builder.buildAll();
25 |
26 | const options = {
27 | build: [
28 | "app",
29 | "--extra-resource=./resources",
30 | "--icon ./public/favicon.ico",
31 | "--platform linux",
32 | "--arch x64",
33 | "--out",
34 | "./dist/linux",
35 | "--overwrite",
36 | ].join(" "),
37 |
38 | package: [
39 | `--src ${path("../dist/linux/app-linux-x64/")}`,
40 | "LiSA",
41 | `--dest ${path("../dist/linux/setup")}`,
42 | "--arch amd64",
43 | `--icon ${path("../utilities/deb/images/icon.ico")}`,
44 | `--background ${path("../utilities/deb/images/background.png")}`,
45 | '--title "LiSA"',
46 | "--overwrite",
47 | ].join(" "),
48 |
49 | spawn: { detached: false, shell: true, stdio: "inherit" },
50 | };
51 |
52 | spawnSync(`electron-packager . ${options.build}`, options.spawn);
53 | spawnSync(`electron-installer-debian ${options.package}`, options.spawn);
54 | };
55 |
56 | /**
57 | * @description - Creates DMG installer for macOS.
58 | * @memberof Packager
59 | *
60 | * @tutorial https://github.com/electron-userland/electron-installer-dmg
61 | */
62 | packageMacOS = () => {
63 | // Build Python & React distribution files
64 | builder.buildAll();
65 |
66 | const options = {
67 | build: [
68 | "app",
69 | "--extra-resource=./resources",
70 | "--icon ./public/favicon.ico",
71 | "--win32",
72 | "--out",
73 | "./dist/mac",
74 | "--overwrite",
75 | ].join(" "),
76 |
77 | package: [
78 | path("../dist/mac/app-darwin-x64/app.app"),
79 | "LiSA",
80 | `--out=${path("../dist/mac/setup")}`,
81 | `--icon=${path("../utilities/dmg/images/icon.icns")}`,
82 | `--background=${path("../utilities/dmg/images/background.png")}`,
83 | '--title="Example app"',
84 | "--overwrite",
85 | ].join(" "),
86 |
87 | spawn: { detached: false, shell: true, stdio: "inherit" },
88 | };
89 |
90 | spawnSync(`electron-packager . ${options.build}`, options.spawn);
91 | spawnSync(`electron-installer-dmg ${options.package}`, options.spawn);
92 | };
93 |
94 | /**
95 | * @description - Creates MSI installer for Windows.
96 | * @memberof Packager
97 | *
98 | * @tutorial https://github.com/felixrieseberg/electron-wix-msi
99 | */
100 | packageWindows = () => {
101 | // eslint-disable-next-line no-console
102 | console.log("Building windows package...");
103 |
104 | // Build Python & React distribution files
105 | builder.buildAll();
106 |
107 | const options = {
108 | app: [
109 | "LiSA",
110 | "--asar",
111 | "--extra-resource=./resources/LiSA",
112 | "--icon ./public/favicon.ico",
113 | "--win32metadata.requested-execution-level=highestAvailable",
114 | "--win32",
115 | "--out",
116 | "./dist/windows",
117 | "--overwrite",
118 | ].join(" "),
119 |
120 | spawn: { detached: false, shell: true, stdio: "inherit" },
121 | };
122 |
123 | spawnSync(`electron-packager . ${options.app}`, options.spawn);
124 |
125 | //////////////////////Msi Build Commented ///////////////////
126 |
127 | // const { MSICreator } = require('electron-wix-msi');
128 |
129 | // const msiCreator = new MSICreator({
130 | // appDirectory: path('../dist/windows/LiSA-win32-x64'),
131 | // appIconPath: path('../utilities/msi/images/icon.ico'),
132 | // description: 'Anime Home',
133 | // exe: 'LiSA',
134 | // manufacturer: 'MF Tek',
135 | // name: 'LiSA',
136 | // outputDirectory: path('../dist/windows/setup'),
137 | // ui: {
138 | // chooseDirectory: true,
139 | // images: {
140 | // background: path('../utilities/msi/images/background.png'),
141 | // banner: path('../utilities/msi/images/banner.png')
142 | // }
143 | // },
144 | // version: '1.0.0'
145 | // });
146 |
147 | // // Customized MSI template
148 | // msiCreator.wixTemplate = msiCreator.wixTemplate
149 | // .replace(/ \(Machine - MSI\)/gi, '')
150 | // .replace(/ \(Machine\)/gi, '');
151 |
152 | // // Create .wxs template and compile MSI
153 | // msiCreator.create().then(() => msiCreator.compile());
154 | };
155 | }
156 |
157 | module.exports.Packager = Packager;
158 |
--------------------------------------------------------------------------------
/src/components/card.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { Box, Heading, Text, Stack, Image, Flex, Badge, Spacer, useToast } from "@chakra-ui/react";
4 | import { AiFillStar } from "react-icons/ai";
5 | import { useDispatch } from "react-redux";
6 | import { useNavigate } from "react-router-dom";
7 | import { addAnimeDetails } from "../store/actions/animeActions";
8 |
9 | export default function Card({ data, query }) {
10 | const navigate = useNavigate();
11 | const toast = useToast();
12 |
13 | const dispatch = useDispatch();
14 |
15 | const exploreCardHandler = () => {
16 | if (query !== "upcoming") {
17 | dispatch(addAnimeDetails(data));
18 | navigate("/anime-details");
19 | } else {
20 | toast({
21 | title: "Anime has not been aired yet! ❤️",
22 | status: "error",
23 | duration: 2000,
24 | });
25 | }
26 | };
27 | return (
28 |
31 |
42 | {/*
43 |

44 |
*/}
45 |
72 |
81 |
82 |
83 |
84 |
85 | {data.anime_type}
86 |
87 |
88 |
89 |
90 | Rank
91 |
92 |
98 | #{data.rank}
99 |
100 |
101 |
102 |
103 |
110 | {data.title}
111 |
112 |
118 |
119 |
120 |
121 | {data.score}
122 |
123 |
124 |
132 |
133 | {data.episodes !== "?" ? "Ep " + data.episodes : "Running"}
134 |
135 |
136 |
137 |
138 |
139 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/backend/scraper/mal.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from bs4 import BeautifulSoup
3 | from typing import Dict, Any
4 | from config import ServerConfig
5 | from utils.headers import get_headers
6 | from .base import Scraper
7 |
8 |
9 | class MyAL(Scraper):
10 | site_url: str = "https://myanimelist.net"
11 | cache: Dict[str, Dict[str, Any]] = {}
12 |
13 | anime_types_dict = {
14 | "all_anime": "",
15 | "airing": "airing",
16 | "upcoming": "upcoming",
17 | "tv": "tv",
18 | "movie": "movie",
19 | "ova": "ova",
20 | "ona": "ona",
21 | "special": "special",
22 | "by_popularity": "bypopularity",
23 | "favorite": "favorite",
24 | }
25 |
26 | manga_types_dict = {
27 | "all_manga": "",
28 | "manga": "manga",
29 | "oneshots": "oneshots",
30 | "doujin": "doujin",
31 | "light_novels": "lightnovels",
32 | "novels": "novels",
33 | "manhwa": "manhwa",
34 | "manhua": "manhua",
35 | "by_popularity": "bypopularity",
36 | "favourite": "favourite"
37 | }
38 |
39 | types_dict = {"anime": anime_types_dict, "manga": manga_types_dict}
40 |
41 | @classmethod
42 | async def get_top_mange(cls, manga_type: str, limit: int = 0):
43 | return await cls.get_top(manga_type, limit, "manga")
44 |
45 | @classmethod
46 | async def get_top_anime(cls, anime_type: str, limit: int = 0):
47 | """request to scrape top anime from MAL website
48 | Args:
49 | anime_type (str): either of ['airing', 'upcoming', 'tv', 'movie', 'ova', 'ona', 'special', 'by_popularity', 'favorite']
50 | limit (str): page number (number of tops in a page)
51 | Returns:
52 | Dict[str, Dict[str, str]]: {
53 | "" : {
54 | "img_url" : (str)url,
55 | "title" : (str),
56 | "anime_type" : (str),
57 | "episodes" : (str),
58 | "score" : (str),
59 | },
60 | ...
61 | "next_top":"api_server_address/top_anime?type=anime_type&limit=limit"
62 | }
63 | """
64 | return await cls.get_top(anime_type, limit, "anime")
65 |
66 | @classmethod
67 | async def get_top(cls, typ: str, limit: int = 0, media: str = "anime") -> Dict[str, Any]:
68 | key = f"{media}_{typ}_{limit}"
69 |
70 | if cls.cache.get(key, None):
71 | return cls.cache[key]
72 |
73 | top_headers = get_headers()
74 |
75 | top_anime_params = {
76 | 'type': cls.types_dict[media][typ],
77 | 'limit': limit,
78 | }
79 |
80 | resp = await cls.get(f'{cls.site_url}/top{media}.php', top_anime_params, top_headers)
81 |
82 | bs_top = BeautifulSoup(await resp.text(), 'html.parser')
83 |
84 | rank = bs_top.find_all("span", {"class": ['rank1', 'rank2', 'rank3', 'rank4']})
85 | ranks = []
86 | for i in rank:
87 | ranks.append(i.text)
88 |
89 | img = bs_top.find_all("img", {"width": "50", "height": "70"})
90 | imgs = []
91 | for x in img:
92 | src = x.get("data-src")
93 | start, end = 0, 0
94 | for i in range(len(src)):
95 | if src[i] == '/' and src[i + 1] == 'r':
96 | start = i
97 | if src[i] == '/' and src[i + 1] == 'i':
98 | end = i
99 | imgs.append(src.replace(src[start:end], ""))
100 |
101 | title_class: str = ""
102 | match media:
103 | case "anime":
104 | title_class = "anime_ranking_h3"
105 | case "manga":
106 | title_class = "manga_h3"
107 |
108 | title = bs_top.find_all("h3", {"class": title_class})
109 |
110 | info = bs_top.find_all("div", {"class": "information"})
111 | segments = []
112 | a_type = []
113 | for x in info:
114 | val = x.text.replace('\n', '').replace(' ', '')
115 | start, end = val.index("("), val.index(")")
116 | segments.append(val[start + 1:end])
117 | a_type.append(val[:start])
118 |
119 | score = bs_top.find_all("span", {"class": [
120 | "score-10", "score-9", "score-8", "score-7", "score-6", "score-5", "score-4", "score-3", "score-2",
121 | "score-1", "score-na"
122 | ]})
123 |
124 | top = []
125 |
126 | for idx, rank in enumerate(ranks):
127 | if rank == "-":
128 | rank = "na"
129 | item = {"rank": rank, "poster": imgs[idx], "title": title[idx].text, "type": a_type[idx],
130 | f"{typ}_detail": f'{ServerConfig.API_SERVER_ADDRESS}/search?type={typ}&query={title[idx].text}&total_res=1'}
131 |
132 | match typ:
133 | case "anime":
134 | item["episodes"] = segments[idx].replace('eps', '')
135 | item["score"] = score[idx * 2].text,
136 | case "manga":
137 | item["volumes"] = segments[idx].replace('vols', ''),
138 |
139 | top.append(item)
140 |
141 | response: Dict[str, Any] = {"data": top}
142 |
143 | try:
144 | next_top = bs_top.find("a", {"class": "next"}).get("href").replace("type", "c")
145 | response["next_top"] = f"{ServerConfig.API_SERVER_ADDRESS}/top{next_top}&type={typ}"
146 | except AttributeError:
147 | response["next_top"] = None
148 |
149 | try:
150 | prev_top = bs_top.find("a", {"class": "prev"}).get("href").replace("type", "c")
151 | response["prev_top"] = f"{ServerConfig.API_SERVER_ADDRESS}/top{prev_top}&type={typ}"
152 | except AttributeError:
153 | response["prev_top"] = None
154 |
155 | cls.cache[key] = response
156 | return response
157 |
--------------------------------------------------------------------------------
/src/screens/homeScreen.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import React, { useEffect } from "react";
4 |
5 | import { useDispatch, useSelector } from "react-redux";
6 | import {
7 | Box,
8 | Flex,
9 | Input,
10 | InputGroup,
11 | InputLeftElement,
12 | InputRightElement,
13 | Kbd,
14 | Text,
15 | } from "@chakra-ui/react";
16 | import { Image } from "@chakra-ui/react";
17 | import { SearchIcon } from "@chakra-ui/icons";
18 |
19 | import { clearEp, clearSearch, searchAnimeList } from "../store/actions/animeActions";
20 | import SearchResultCard from "../components/search-result-card";
21 | import NetworkError from "../components/network-error";
22 | import useNetworkStatus from "../hooks/useNetworkStatus";
23 |
24 | import NotFoundImg from "src/assets/img/not-found.png";
25 | import LoaderSearchGif from "src/assets/img/loader-serch.gif";
26 | import HomeScreenLogoImg from "src/assets/img/home_screen_logo.png";
27 |
28 | export const HomeScreen = () => {
29 | const { isOnline } = useNetworkStatus();
30 |
31 | const dispatch = useDispatch();
32 | const [query, setQuery] = React.useState("");
33 | const handleSearchChange = (event) => {
34 | setQuery(event.target.value);
35 | };
36 |
37 | const { animes, loading, error } = useSelector((state) => state.animeSearchList);
38 |
39 | const handleKeyDown = (event) => {
40 | if (event.key === "Enter" && query) {
41 | dispatch(searchAnimeList(query));
42 | dispatch(clearEp());
43 | }
44 | if (!query) {
45 | dispatch(clearSearch());
46 | }
47 | };
48 |
49 | useEffect(() => {
50 | return () => {
51 | dispatch(clearSearch());
52 | };
53 | }, []);
54 |
55 | return (
56 |
57 | {isOnline ? (
58 |
65 | {" "}
66 |
67 |
68 |
72 | Enter
73 |
74 | }
75 | />
76 | }
79 | />
80 |
81 |
89 |
90 |
91 | {!loading && animes && (
92 |
113 | {animes.map((anime) => {
114 | return (
115 |
121 | );
122 | })}
123 |
124 | )}
125 | {!loading && error && (
126 |
127 |
135 |
136 | Anime Not Found
137 |
138 |
139 | The result you're looking for does not seem to exist
140 |
141 |
142 | )}
143 | {loading && }
144 |
145 | ) : (
146 |
147 | )}
148 |
149 | );
150 | };
151 |
--------------------------------------------------------------------------------
/src/components/search-result-card.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import {
3 | Badge,
4 | Box,
5 | Button,
6 | Center,
7 | Flex,
8 | Heading,
9 | Image,
10 | Spacer,
11 | Stack,
12 | Text,
13 | useColorModeValue,
14 | } from "@chakra-ui/react";
15 | import { Link } from "react-router-dom";
16 | import { useDispatch } from "react-redux";
17 | import { AiFillStar } from "react-icons/ai";
18 |
19 | import { addAnimeDetails } from "../store/actions/animeActions";
20 |
21 | export default function SearchResultCard({ data, cardWidth, cardMargin, maxImgWidth }) {
22 |
23 | const dispatch = useDispatch();
24 |
25 | const detailsClickHandler = () => {
26 | dispatch(addAnimeDetails(data));
27 | };
28 |
29 | return (
30 |
42 |
43 |
53 | {/*
54 |

55 |
*/}
56 |
83 |
92 |
93 |
94 |
95 |
96 | {data.type}
97 |
98 |
99 | {/*
100 |
105 | Rank
106 |
107 |
114 | #{data.rank}
115 |
116 | */}
117 |
118 |
119 |
126 | {data.jp_name ? `${data.jp_name}` : ""}
127 | {data.eng_name ? ` | ${data.eng_name}` : ""}
128 |
129 |
135 |
136 |
137 |
138 | {data.score}
139 |
140 |
141 |
149 |
150 | {data.episodes !== "?"
151 | ? "Ep " + data.no_of_episodes
152 | : "Running"}
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | );
161 | };
--------------------------------------------------------------------------------
/src/store/reducers/animeReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ANIME_CURRENT_EP_FAIL,
3 | ANIME_CURRENT_EP_REQUEST,
4 | ANIME_CURRENT_EP_SUCCESS,
5 | ANIME_DETAILS_FAIL,
6 | ANIME_DETAILS_REQUEST,
7 | ANIME_DETAILS_SUCCESS,
8 | ANIME_EPISODES_ADD_FAIL,
9 | ANIME_EPISODES_ADD_REQUEST,
10 | ANIME_EPISODES_ADD_SUCCESS,
11 | ANIME_EXPLORE_DETAILS_FAIL,
12 | ANIME_EXPLORE_DETAILS_REQUEST,
13 | ANIME_EXPLORE_DETAILS_SUCCESS,
14 | ANIME_RECOMMENDATION_FAIL,
15 | ANIME_RECOMMENDATION_REQUEST,
16 | ANIME_RECOMMENDATION_SUCCESS,
17 | ANIME_SEARCH_CLEAR,
18 | ANIME_SEARCH_FAIL,
19 | ANIME_SEARCH_REQUEST,
20 | ANIME_SEARCH_SUCCESS,
21 | ANIME_STREAM_DETAILS_CLEAR,
22 | ANIME_STREAM_DETAILS_FAIL,
23 | ANIME_STREAM_DETAILS_REQUEST,
24 | ANIME_STREAM_DETAILS_SUCCESS,
25 | ANIME_STREAM_EXTERNAL_CLEAR,
26 | ANIME_STREAM_EXTERNAL_FAIL,
27 | ANIME_STREAM_EXTERNAL_REQUEST,
28 | ANIME_STREAM_EXTERNAL_SUCCESS,
29 | ANIME_STREAM_URL_CLEAR,
30 | ANIME_STREAM_URL_FAIL,
31 | ANIME_STREAM_URL_REQUEST,
32 | ANIME_STREAM_URL_SUCCESS,
33 | } from "../constants/animeConstants";
34 |
35 | export const animeSearchListReducer = (state = { animes: null }, action) => {
36 | switch (action.type) {
37 | case ANIME_SEARCH_REQUEST:
38 | return {
39 | loading: true,
40 | animes: action.payload,
41 | };
42 |
43 | case ANIME_SEARCH_SUCCESS:
44 | return { loading: false, animes: action.payload };
45 |
46 | case ANIME_SEARCH_FAIL:
47 | return { loading: false, animes: null, error: action.payload };
48 | case ANIME_SEARCH_CLEAR:
49 | return { loading: false, animes: null };
50 |
51 | default:
52 | return state;
53 | }
54 | };
55 |
56 | export const animeEpisodesReducer = (state = {}, action) => {
57 | switch (action.type) {
58 | case ANIME_EPISODES_ADD_REQUEST:
59 | return {
60 | loading: true,
61 | // @ts-ignore
62 | details: { ...state.details, ...action.payload },
63 | };
64 |
65 | case ANIME_EPISODES_ADD_SUCCESS:
66 | return { loading: false, details: action.payload };
67 |
68 | case ANIME_EPISODES_ADD_FAIL:
69 | return { loading: false, details: action.payload };
70 |
71 | default:
72 | return state;
73 | }
74 | };
75 | export const animeCurrentEpReducer = (state = {}, action) => {
76 | switch (action.type) {
77 | case ANIME_CURRENT_EP_REQUEST:
78 | return { loading: true, details: action.payload };
79 |
80 | case ANIME_CURRENT_EP_SUCCESS:
81 | return { loading: false, details: action.payload };
82 |
83 | case ANIME_CURRENT_EP_FAIL:
84 | return { loading: false, details: action.payload };
85 |
86 | default:
87 | return state;
88 | }
89 | };
90 | export const animeDetailsReducer = (state = {}, action) => {
91 | switch (action.type) {
92 | case ANIME_DETAILS_REQUEST:
93 | return { loading: true, details: action.payload };
94 |
95 | case ANIME_DETAILS_SUCCESS:
96 | return { loading: false, details: action.payload };
97 |
98 | case ANIME_DETAILS_FAIL:
99 | return { loading: false, details: action.payload };
100 |
101 | default:
102 | return state;
103 | }
104 | };
105 | export const animeEpUrlReducer = (
106 | state = {
107 | loading: null,
108 | },
109 | action
110 | ) => {
111 | switch (action.type) {
112 | case ANIME_STREAM_URL_REQUEST:
113 | return { loading: true, url: action.payload };
114 |
115 | case ANIME_STREAM_URL_SUCCESS:
116 | return { loading: false, url: action.payload };
117 |
118 | case ANIME_STREAM_URL_FAIL:
119 | return { loading: false, url: action.payload };
120 | case ANIME_STREAM_URL_CLEAR:
121 | return { loading: false, url: "" };
122 |
123 | default:
124 | return state;
125 | }
126 | };
127 |
128 | //StreamDetails
129 | export const animeStreamDetailsReducer = (state = { details: null }, action) => {
130 | switch (action.type) {
131 | case ANIME_STREAM_DETAILS_REQUEST:
132 | return {
133 | loading: true,
134 | details: null,
135 | };
136 |
137 | case ANIME_STREAM_DETAILS_SUCCESS:
138 | return { loading: false, details: action.payload };
139 |
140 | case ANIME_STREAM_DETAILS_FAIL:
141 | return { loading: false, error: action.payload };
142 | case ANIME_STREAM_DETAILS_CLEAR:
143 | return { loading: false, details: null };
144 |
145 | default:
146 | return state;
147 | }
148 | };
149 | //StreamDetails
150 | export const animeExploreDetailsReducer = (state = { details: null }, action) => {
151 | switch (action.type) {
152 | case ANIME_EXPLORE_DETAILS_REQUEST:
153 | return {
154 | loading: true,
155 | details: null,
156 | };
157 |
158 | case ANIME_EXPLORE_DETAILS_SUCCESS:
159 | return { loading: false, details: action.payload };
160 |
161 | case ANIME_EXPLORE_DETAILS_FAIL:
162 | return { loading: false, error: action.payload };
163 | case ANIME_STREAM_DETAILS_CLEAR:
164 | return { loading: false, details: null };
165 |
166 | default:
167 | return state;
168 | }
169 | };
170 |
171 | //Stream
172 | export const animeStreamExternalReducer = (state = { details: null }, action) => {
173 | switch (action.type) {
174 | case ANIME_STREAM_EXTERNAL_REQUEST:
175 | return {
176 | loading: true,
177 | details: null,
178 | };
179 |
180 | case ANIME_STREAM_EXTERNAL_SUCCESS:
181 | return { loading: false, details: action.payload };
182 |
183 | case ANIME_STREAM_EXTERNAL_FAIL:
184 | return { loading: false, error: action.payload };
185 | case ANIME_STREAM_EXTERNAL_CLEAR:
186 | return { loading: false, details: null };
187 |
188 | default:
189 | return state;
190 | }
191 | };
192 |
193 | export const recommendationsReducer = (state = { details: null }, action) => {
194 | switch (action.type) {
195 | case ANIME_RECOMMENDATION_REQUEST:
196 | return {
197 | loading: true,
198 | details: null,
199 | };
200 |
201 | case ANIME_RECOMMENDATION_SUCCESS:
202 | return { loading: false, details: action.payload };
203 |
204 | case ANIME_RECOMMENDATION_FAIL:
205 | return { loading: false, error: action.payload };
206 |
207 | default:
208 | return state;
209 | }
210 | };
211 |
--------------------------------------------------------------------------------
/src/components/video-player.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { Box, Image, useDisclosure, useToast } from "@chakra-ui/react";
4 | import { useEffect, useState, useRef } from "react";
5 | import videojs from "video.js";
6 | import "videojs-contrib-quality-levels";
7 | import qualitySelector from "videojs-hls-quality-selector";
8 | import "video.js/dist/video-js.css";
9 | import "videojs-hotkeys";
10 | import ExternalPlayerPopup from "./externalPopup";
11 | import { useDispatch, useSelector } from "react-redux";
12 | // function getWindowSize() {
13 | // const { innerWidth, innerHeight } = window;
14 | // return { innerWidth, innerHeight };
15 | // }
16 | import hlsQualitySelector from "videojs-hls-quality-selector";
17 | import { downloadVideo } from "../store/actions/downloadActions";
18 |
19 | export default function VideoPlayer({
20 | url,
21 | epDetails,
22 | player,
23 | setPlayer,
24 | prevTime,
25 | nextEpHandler,
26 | streamLoading,
27 | setQualityOptions,
28 | qualityOptions,
29 | }) {
30 | const toast = useToast();
31 |
32 | const { details } = useSelector((state) => state.animeStreamDetails);
33 | const dispatch = useDispatch();
34 | const { isOpen, onOpen, onClose } = useDisclosure();
35 | const [language, setLanguage] = useState("jpn");
36 | const videoRef = useRef();
37 | const [callFinishVideoAPI, setCallFinishVideoAPI] = useState(false);
38 | const [vidDuration, setVidDuration] = useState(50000);
39 | const [downloadUrl, setDownloadUrl] = useState(null);
40 | const [downloadLoading, setDownloadLoading] = useState(false);
41 |
42 | useEffect(() => {
43 | if (player && url) {
44 | player.src({
45 | src: url,
46 | type: "application/x-mpegURL",
47 | withCredentials: false,
48 | });
49 | player.poster("");
50 | setCallFinishVideoAPI(false);
51 | }
52 |
53 | if (player && prevTime) {
54 | if (prevTime) {
55 | player?.currentTime(prevTime);
56 | player?.play();
57 | } else {
58 | player?.currentTime(0);
59 | }
60 | }
61 | }, [url]);
62 |
63 | useEffect(() => {
64 | if (callFinishVideoAPI) {
65 | nextEpHandler();
66 | }
67 | }, [callFinishVideoAPI]);
68 |
69 | useEffect(() => {
70 | videojs.registerPlugin("hlsQualitySelector", hlsQualitySelector);
71 |
72 | const videoJsOptions = {
73 | autoplay: false,
74 | preload: "metadata",
75 | playbackRates: [0.5, 1, 1.5, 2],
76 |
77 | controls: true,
78 | poster: epDetails?.details?.snapshot,
79 | controlBar: {
80 | pictureInPictureToggle: false,
81 | },
82 | fluid: true,
83 | sources: [
84 | {
85 | src: url,
86 | type: "application/x-mpegURL",
87 | withCredentials: false,
88 | },
89 | ],
90 | html5: {
91 | nativeAudioTracks: true,
92 | nativeVideoTracks: true,
93 | nativeTextTracks: true,
94 | },
95 | };
96 |
97 | const plyer = videojs(videoRef.current, videoJsOptions, function onPlayerReady() {
98 | this.hotkeys({
99 | volumeStep: 0.1,
100 | seekStep: 5,
101 | enableModifiersForNumbers: false,
102 | });
103 | });
104 | var fullscreen = plyer.controlBar.getChild("FullscreenToggle");
105 | var index = plyer.controlBar.children().indexOf(fullscreen);
106 | var externalPlayerButton = plyer.controlBar.addChild("button", {}, index);
107 |
108 | var externalPlayerButtonDom = externalPlayerButton.el();
109 | if (externalPlayerButtonDom) {
110 | externalPlayerButtonDom.innerHTML = "external";
111 |
112 | externalPlayerButtonDom.onclick = function () {
113 | if (plyer.isFullscreen()) {
114 | fullscreen.handleClick();
115 | }
116 | onOpen();
117 | };
118 | }
119 |
120 | let qualityLevels = plyer.qualityLevels();
121 |
122 | setQualityOptions(qualityLevels.levels_);
123 |
124 | // qualityLevels.on('change', function() {
125 | // console.log('Quality Level changed!');
126 | // console.log('New level:', qualityLevels[qualityLevels.selectedIndex]);
127 | // });
128 | // var downloadButton = plyer.controlBar.addChild("button", {}, index);
129 |
130 | // var downloadButtonDom = downloadButton.el();
131 | // if (downloadButtonDom) {
132 | // downloadButtonDom.style.width = "2em";
133 | // downloadButtonDom.innerHTML = `
`;
134 |
135 | // downloadButtonDom.onclick = function () {
136 |
137 | // }
138 |
139 | setPlayer(plyer);
140 |
141 | return () => {
142 | if (player) player.dispose();
143 | };
144 | }, [epDetails]);
145 |
146 | useEffect(() => {
147 | if (player && player.hlsQualitySelector) {
148 | player.hlsQualitySelector = hlsQualitySelector;
149 |
150 | player.hlsQualitySelector({ displayCurrentQuality: true });
151 | let qualityLevels = player.qualityLevels();
152 | setQualityOptions(qualityLevels.levels_);
153 | }
154 | }, [player]);
155 |
156 | // useEffect(() => {
157 |
158 | // if (player && prevTime) {
159 | // if (prevTime) {
160 | // player?.currentTime(prevTime);
161 | // player?.play();
162 | // } else {
163 | // console.log("kokoko")
164 | // player?.currentTime(0);
165 | // }
166 | // }
167 |
168 | // return () => {
169 | // if (player) player.dispose();
170 | // };
171 | // }, [prevTime]);
172 |
173 | return (
174 |
175 |
176 |
189 |
190 |
191 |
192 |
193 | );
194 | };
--------------------------------------------------------------------------------
/src/store/actions/animeActions.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import server from "../../utils/axios";
4 | import {
5 | ANIME_CURRENT_EP_FAIL,
6 | ANIME_CURRENT_EP_REQUEST,
7 | ANIME_CURRENT_EP_SUCCESS,
8 | ANIME_DETAILS_FAIL,
9 | ANIME_DETAILS_REQUEST,
10 | ANIME_DETAILS_SUCCESS,
11 | ANIME_EPISODES_ADD_FAIL,
12 | ANIME_EPISODES_ADD_REQUEST,
13 | ANIME_EPISODES_ADD_SUCCESS,
14 | ANIME_EXPLORE_DETAILS_FAIL,
15 | ANIME_EXPLORE_DETAILS_REQUEST,
16 | ANIME_EXPLORE_DETAILS_SUCCESS,
17 | ANIME_RECOMMENDATION_FAIL,
18 | ANIME_RECOMMENDATION_REQUEST,
19 | ANIME_RECOMMENDATION_SUCCESS,
20 | ANIME_SEARCH_CLEAR,
21 | ANIME_SEARCH_FAIL,
22 | ANIME_SEARCH_REQUEST,
23 | ANIME_SEARCH_SUCCESS,
24 | ANIME_STREAM_DETAILS_FAIL,
25 | ANIME_STREAM_DETAILS_REQUEST,
26 | ANIME_STREAM_DETAILS_SUCCESS,
27 | ANIME_STREAM_EXTERNAL_CLEAR,
28 | ANIME_STREAM_EXTERNAL_FAIL,
29 | ANIME_STREAM_EXTERNAL_REQUEST,
30 | ANIME_STREAM_EXTERNAL_SUCCESS,
31 | ANIME_STREAM_URL_CLEAR,
32 | ANIME_STREAM_URL_FAIL,
33 | ANIME_STREAM_URL_REQUEST,
34 | ANIME_STREAM_URL_SUCCESS,
35 | DOWNLOAD_LIBRARY_FAIL,
36 | DOWNLOAD_LIBRARY_REQUEST,
37 | DOWNLOAD_LIBRARY_SUCCESS,
38 | } from "../constants/animeConstants";
39 |
40 | export const searchAnimeList = (query) => async (dispatch) => {
41 | try {
42 | dispatch({ type: ANIME_SEARCH_REQUEST, payload: {} });
43 |
44 | const { data } = await server.get(`/search?type=anime&query=${query}`);
45 | dispatch({ type: ANIME_SEARCH_SUCCESS, payload: data });
46 | } catch (error) {
47 | dispatch({ type: ANIME_SEARCH_FAIL, payload: error.response.data });
48 | }
49 | };
50 |
51 | export const addAnimeDetails = (data) => async (dispatch) => {
52 | try {
53 | let url;
54 | let ep_details;
55 | dispatch({ type: ANIME_DETAILS_REQUEST });
56 | dispatch({ type: ANIME_DETAILS_SUCCESS, payload: data });
57 | dispatch({ type: ANIME_EPISODES_ADD_REQUEST, payload: data });
58 |
59 | if (data.anime_detail) {
60 | // dispatch({ type: ANIME_EPISODES_ADD_REQUEST });
61 |
62 | const searchRes = await server.get(data.anime_detail);
63 |
64 | ep_details = await server.get(searchRes.data[0].ep_details);
65 | dispatch({
66 | type: ANIME_DETAILS_SUCCESS,
67 | payload: { ...data, ...ep_details.data },
68 | });
69 | } else {
70 | dispatch({ type: ANIME_DETAILS_SUCCESS, payload: data });
71 |
72 | ep_details = await server.get(data.ep_details);
73 | }
74 |
75 | dispatch(addEpisodesDetails(ep_details.data));
76 | } catch (error) {
77 | dispatch({ type: ANIME_DETAILS_FAIL, payload: error });
78 | }
79 | };
80 |
81 | export const addEpisodesDetails = (data) => async (dispatch, getState) => {
82 | try {
83 | dispatch({ type: ANIME_EPISODES_ADD_REQUEST });
84 | let { details } = getState().animeEpisodesDetails;
85 | let allDetails;
86 | if (details) {
87 | allDetails = { ...details, ...data };
88 | } else {
89 | allDetails = data;
90 | }
91 | dispatch({ type: ANIME_EPISODES_ADD_SUCCESS, payload: allDetails });
92 | } catch (error) {
93 | dispatch({ type: ANIME_EPISODES_ADD_FAIL, payload: error });
94 | }
95 | };
96 | export const addCurrentEp = (data) => async (dispatch) => {
97 | try {
98 | dispatch({ type: ANIME_CURRENT_EP_REQUEST });
99 |
100 | dispatch({ type: ANIME_CURRENT_EP_SUCCESS, payload: data });
101 | } catch (error) {
102 | dispatch({ type: ANIME_CURRENT_EP_FAIL, payload: error });
103 | }
104 | };
105 | export const clearSearch = () => async (dispatch) => {
106 | dispatch({ type: ANIME_SEARCH_CLEAR });
107 | };
108 | export const clearEp = () => async (dispatch) => {
109 | dispatch({ type: ANIME_STREAM_URL_CLEAR });
110 | };
111 |
112 | export const getStreamDetails = (stream_detail) => async (dispatch) => {
113 | try {
114 | dispatch({ type: ANIME_STREAM_DETAILS_REQUEST });
115 | const { data } = await server.get(stream_detail);
116 |
117 | dispatch({ type: ANIME_STREAM_DETAILS_SUCCESS, payload: data });
118 | } catch (error) {
119 | dispatch({ type: ANIME_STREAM_DETAILS_FAIL, payload: error });
120 | }
121 | };
122 |
123 | export const getExploreDetails = (query) => async (dispatch) => {
124 | try {
125 | dispatch({ type: ANIME_EXPLORE_DETAILS_REQUEST });
126 | const { data } = await server.get(`top_anime?type=${query}&limit=0`);
127 |
128 | dispatch({ type: ANIME_EXPLORE_DETAILS_SUCCESS, payload: data });
129 | // dispatch({ type: ANIME_EXPLORE_QUERY, payload: query });
130 | } catch (error) {
131 | dispatch({ type: ANIME_EXPLORE_DETAILS_FAIL, payload: error });
132 | }
133 | };
134 |
135 | export const playVideoExternal = (payload) => async (dispatch) => {
136 | try {
137 | dispatch({ type: ANIME_STREAM_EXTERNAL_REQUEST });
138 | await server.post(`/stream`, payload, {
139 | "Content-Type": "application/json",
140 | });
141 |
142 | dispatch({ type: ANIME_STREAM_EXTERNAL_SUCCESS });
143 | } catch (error) {
144 | dispatch({
145 | type: ANIME_STREAM_EXTERNAL_FAIL,
146 | payload: error.response.data,
147 | });
148 |
149 | setTimeout(() => {
150 | dispatch({
151 | type: ANIME_STREAM_EXTERNAL_CLEAR,
152 | });
153 | }, 3000);
154 | throw new Error(error);
155 | }
156 | };
157 | export const getVideoUrl = (pahewin_url) => async (dispatch) => {
158 | try {
159 | dispatch({ type: ANIME_STREAM_URL_REQUEST });
160 | const { data } = await server.post(
161 | `/get_video_url`,
162 | {
163 | pahewin_url,
164 | },
165 | {
166 | "Content-Type": "application/json",
167 | }
168 | );
169 |
170 | dispatch({ type: ANIME_STREAM_URL_SUCCESS, payload: data });
171 | } catch (error) {
172 | dispatch({ type: ANIME_STREAM_URL_FAIL, payload: error });
173 | }
174 | };
175 | export const cancelLiveDownload = async (id) => {
176 | try {
177 | const { data } = await server.post(
178 | `/download/cancel`,
179 | {
180 | id: [id],
181 | },
182 | {
183 | "Content-Type": "application/json",
184 | }
185 | );
186 | } catch (error) {
187 | console.log(error);
188 | }
189 | };
190 | export const pauseLiveDownload = async (id) => {
191 | try {
192 | const { data } = await server.post(
193 | `/download/pause`,
194 | {
195 | id: [id],
196 | },
197 | {
198 | "Content-Type": "application/json",
199 | }
200 | );
201 | } catch (error) {
202 | console.log(error);
203 | }
204 | };
205 | export const resumeLiveDownload = async (id) => {
206 | try {
207 | const { data } = await server.post(
208 | `/download/resume`,
209 | {
210 | id: [id],
211 | },
212 | {
213 | "Content-Type": "application/json",
214 | }
215 | );
216 | } catch (error) {
217 | console.log(error);
218 | }
219 | };
220 |
221 | export const getDownloadHistory = () => async (dispatch) => {
222 | try {
223 | dispatch({ type: DOWNLOAD_LIBRARY_REQUEST });
224 |
225 | const { data } = await server.get(`/library`);
226 |
227 | dispatch({ type: DOWNLOAD_LIBRARY_SUCCESS, payload: data });
228 | } catch (error) {
229 | dispatch({ type: DOWNLOAD_LIBRARY_FAIL, payload: error });
230 | }
231 | };
232 | export const getRecommendations = (url) => async (dispatch) => {
233 | try {
234 | dispatch({ type: ANIME_RECOMMENDATION_REQUEST });
235 |
236 | const { data } = await server.get(url);
237 |
238 | dispatch({ type: ANIME_RECOMMENDATION_SUCCESS, payload: data });
239 | } catch (error) {
240 | dispatch({ type: ANIME_RECOMMENDATION_FAIL, payload: error });
241 | }
242 | };
243 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, ipcMain } = require("electron");
2 | const path = require("path");
3 | const isDev = require("electron-is-dev");
4 | const { spawn, fork } = require("child_process");
5 | const isDevMode = require("electron-is-dev");
6 | const psTree = require("ps-tree");
7 |
8 | /**
9 | * @namespace BrowserWindow
10 | * @description - Electron browser windows.
11 | */
12 | const browserWindows = {};
13 | let pids = [];
14 |
15 | /**
16 | * @description - Creates main window.
17 | * @param {number} port - Port that Python server is running on.
18 | *
19 | * @memberof BrowserWindow
20 | */
21 | const createMainWindow = () => {
22 | const { loadingWindow, mainWindow } = browserWindows;
23 |
24 | /**
25 | * @description - Function to use custom JavaSCript in the DOM.
26 | * @param {string} command - JavaScript to execute in DOM.
27 | * @param {function} callback - Callback to execute here once complete.
28 | * @returns {Promise}
29 | */
30 | const executeOnWindow = (command, callback) => {
31 | return mainWindow.webContents
32 | .executeJavaScript(command)
33 | .then(callback)
34 | .catch(console.error);
35 | };
36 |
37 | /**
38 | * If in developer mode, show a loading window while
39 | * the app and developer server compile.
40 | */
41 | const isPageLoaded = `
42 | var isBodyFull = document.body.innerHTML !== "";
43 | var isHeadFull = document.head.innerHTML !== "";
44 | var isLoadSuccess = isBodyFull && isHeadFull;
45 |
46 | isLoadSuccess || Boolean(location.reload());
47 | `;
48 |
49 | /**
50 | * @description Updates windows if page is loaded
51 | * @param {*} isLoaded
52 | */
53 | const handleLoad = (isLoaded) => {
54 | if (isLoaded) {
55 | /**
56 | * Keep show() & hide() in this order to prevent
57 | * unresponsive behavior during page load.
58 | */
59 |
60 | mainWindow.show();
61 | loadingWindow.hide();
62 | loadingWindow.close();
63 | }
64 | };
65 |
66 | /**
67 | * Checks if the page has been populated with
68 | * React project. if so, shows the main page.
69 | */
70 | // executeOnWindow(isPageLoaded, handleLoad);
71 |
72 | if (isDevMode) {
73 | mainWindow.loadURL("http://localhost:3000");
74 |
75 | mainWindow.hide();
76 |
77 | /**
78 | * Hide loading window and show main window
79 | * once the main window is ready.
80 | */
81 | mainWindow.webContents.on("did-finish-load", () => {
82 | /**
83 | * Checks page for errors that may have occurred
84 | * during the hot-loading process.
85 | */
86 | // mainWindow.webContents.openDevTools({ mode: "undocked" });
87 |
88 | /**
89 | * Checks if the page has been populated with
90 | * React project. if so, shows the main page.
91 | */
92 | executeOnWindow(isPageLoaded, handleLoad);
93 | });
94 | } else {
95 | mainWindow.hide();
96 |
97 | mainWindow.removeMenu(true);
98 |
99 | mainWindow.loadFile(path.join(__dirname, "build/index.html"));
100 | // mainWindow.webContents.openDevTools({ mode: "undocked" });
101 |
102 | mainWindow.webContents.on("did-finish-load", () => {
103 | executeOnWindow(isPageLoaded, handleLoad);
104 | });
105 | }
106 | };
107 |
108 | /**
109 | * @description - Creates loading window to show while build is created.
110 | * @memberof BrowserWindow
111 | */
112 | const createLoadingWindow = () => {
113 | return new Promise((resolve, reject) => {
114 | const { loadingWindow } = browserWindows;
115 |
116 | // Variants of developer loading screen
117 | const loaderConfig = {
118 | main: "public/loader.html",
119 | };
120 |
121 | try {
122 | loadingWindow.loadFile(path.join(__dirname, loaderConfig.main));
123 | loadingWindow.removeMenu(true);
124 |
125 | loadingWindow.webContents.on("did-finish-load", () => {
126 | loadingWindow.show();
127 | resolve();
128 | });
129 | } catch (error) {
130 | console.error(error);
131 | reject();
132 | }
133 | });
134 | };
135 |
136 | app.whenReady().then(async () => {
137 | /**
138 | * Method to set port in range of 3001-3999,
139 | * based on availability.
140 | */
141 |
142 | /**
143 | * Assigns the main browser window on the
144 | * browserWindows object.
145 | */
146 | browserWindows.mainWindow = new BrowserWindow({
147 | show: false,
148 | webPreferences: {
149 | contextIsolation: false,
150 | enableRemoteModule: true,
151 | autoHideMenuBar: true,
152 | show: false,
153 | nodeIntegration: true,
154 | preload: path.join(app.getAppPath(), "preload.js"),
155 | },
156 | });
157 |
158 | /**
159 | * If not using in production, use the loading window
160 | * and run Python in shell.
161 | */
162 | if (isDevMode) {
163 | // await installExtensions(); // React, Redux devTools
164 | browserWindows.loadingWindow = new BrowserWindow({
165 | frame: false,
166 | transparent: true,
167 | alwaysOnTop: true,
168 | width: 300,
169 | height: 300,
170 | });
171 | createLoadingWindow().then(() => createMainWindow());
172 | // var devProc = spawn(`python backend/LiSA.py`, {
173 | // detached: true,
174 | // shell: true,
175 | // stdio: "inherit",
176 | // });
177 | var devProc = spawn("python backend/LiSA.py", {
178 | detached: true,
179 | shell: true,
180 | });
181 | } else {
182 | /**
183 | * If using in production, use the main window
184 | * and run bundled app (dmg, elf, or exe) file.
185 | */
186 | browserWindows.loadingWindow = new BrowserWindow({
187 | frame: false,
188 | transparent: true,
189 | alwaysOnTop: true,
190 | });
191 | createLoadingWindow().then(() => {
192 | createMainWindow();
193 | });
194 | // Dynamic script assignment for starting Python in production
195 | const runPython = {
196 | darwin: `open -gj "${path.join(app.getAppPath(), "resources", "app.app")}" --args`,
197 | linux: "./resources/main/main",
198 | win32: `powershell -Command Start-Process -WindowStyle Hidden "./resources/LiSA/LiSA.exe"`,
199 | }[process.platform];
200 |
201 | var proc = spawn(`${runPython}`, {
202 | shell: true,
203 | });
204 | }
205 |
206 | app.on("activate", () => {
207 | /**
208 | * On macOS it's common to re-create a window in the app when the
209 | * dock icon is clicked and there are no other windows open.
210 | */
211 | if (BrowserWindow.getAllWindows().length === 0) createMainWindow();
212 | });
213 |
214 | /**
215 | * Ensures that only a single instance of the app
216 | * can run, this correlates with the "name" property
217 | * used in `package.json`.
218 | */
219 | const initialInstance = app.requestSingleInstanceLock();
220 | if (!initialInstance) app.quit();
221 | else {
222 | app.on("second-instance", () => {
223 | if (browserWindows.mainWindow?.isMinimized()) browserWindows.mainWindow?.restore();
224 | browserWindows.mainWindow?.focus();
225 | });
226 | }
227 |
228 | /**
229 | * Quit when all windows are closed, except on macOS. There, it's common
230 | * for applications and their menu bar to stay active until the user quits
231 | * explicitly with Cmd + Q.
232 | */
233 |
234 | // app.on('before-quit', function() {
235 | // pids.forEach(function(pid) {
236 | // // A simple pid lookup
237 | // ps.kill( pid, function( err ) {
238 | // if (err) {
239 | // throw new Error( err );
240 | // }
241 | // else {
242 | // console.log( 'Process %s has been killed!', pid );
243 | // }
244 | // });
245 | // });
246 | // });
247 |
248 | app.on("window-all-closed", () => {
249 | console.log("inside close");
250 |
251 | if (process.platform !== "darwin") {
252 | // console.log("killing");
253 | // console.log(devProc.pid);
254 |
255 | // // spawn(`powershell.exe -Command kill ${devProc.pid}`);
256 |
257 | // devProc.kill("SIGHUP");
258 |
259 | // psTree(devProc.pid, function (err, children) {
260 | // console.log(`asdasdasdasdasd`)
261 | // console.log(err)
262 | // console.log(children)
263 | // devProc.spawn(
264 | // "kill",
265 | // ["-9"].concat(
266 | // children.map(function (p) {
267 | // console.log(`inside map child`)
268 | // console.log(children)
269 | // return p.PID;
270 | // })
271 | // )
272 | // );
273 | // });
274 |
275 | spawn("taskkill /IM LiSA.exe /F", {
276 | shell: true,
277 | detached: true,
278 | });
279 |
280 | app.quit();
281 | console.log("after quit");
282 | }
283 | });
284 | });
285 |
--------------------------------------------------------------------------------
/backend/scraper/manga_scraper.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from abc import abstractmethod
3 | from bs4 import BeautifulSoup
4 | from typing import Dict, List, Any
5 | from config import ServerConfig
6 | from utils.headers import get_headers
7 | import re
8 | from .base import Scraper
9 |
10 |
11 | class Manga(Scraper):
12 | _SITE_NAME: str = None
13 | site_url: str
14 | api_url: str
15 | manifest_header: dict = get_headers()
16 | default_poster: str = "def_manga.png"
17 | _SCRAPERS: Dict[str, object] = {}
18 |
19 | def __init_subclass__(cls, **kwargs):
20 | super().__init_subclass__(**kwargs)
21 | cls._SCRAPERS[cls._SITE_NAME] = cls
22 |
23 | @classmethod
24 | def get_scraper(cls, site_name: str) -> Manga:
25 | return cls._SCRAPERS.get(site_name.lower(), None)
26 |
27 | @abstractmethod
28 | def search_manga(self, manga_name: str, search_by: str = "book_name") -> List[Dict[str, str]]:
29 | ...
30 |
31 | @abstractmethod
32 | def get_chp_session(self, manga_session: str) -> List[Dict[int, Dict[str, str]]]:
33 | ...
34 |
35 | @abstractmethod
36 | def get_manga_source_data(self, chp_session: str) -> Dict[str, str]:
37 | ...
38 |
39 | @abstractmethod
40 | def get_recommendation(self, manga_session: str) -> List[Dict[str, str]]:
41 | ...
42 |
43 |
44 | class MangaKatana(Manga):
45 | _SITE_NAME: str = "mangakatana"
46 | site_url: str = "https://mangakatana.com"
47 | api_url: str = "https://mangakatana.com"
48 |
49 | async def search_manga(self, manga_name: str, search_by: str = "book_name", page_no: int = 1, total_res: int = 20) -> Dict[str, Any]:
50 |
51 | resp_text = await self.get(f"{self.site_url}/page/{page_no}", data={"search": manga_name, "search_by": search_by})
52 |
53 | resp_text = await resp_text.text()
54 |
55 | resp = {"response": List[Dict[str, str]]}
56 |
57 | search_bs = BeautifulSoup(resp_text, 'html.parser')
58 |
59 | if len(search_bs.find("title").text) != len(manga_name):
60 | scrape_func = self.__scrape_list
61 |
62 | pag_list = search_bs.find("ul", {"class": "uk-pagination"}) # check if multiple pages exists or not
63 |
64 | if pag_list:
65 |
66 | resp["prev"] = f"{ServerConfig.API_SERVER_ADDRESS}/search?type=manga&page={int(page_no) - 1}&query={manga_name}" if pag_list.find(
67 | "a", {"class": "prev"}) else None
68 |
69 | resp["next"] = f"{ServerConfig.API_SERVER_ADDRESS}/search?type=manga&page={int(page_no) + 1}&query={manga_name}" if pag_list.find(
70 | "a", {"class": "next"}) else None
71 | else:
72 | scrape_func = self.__scrape_detail
73 |
74 | manga_list = scrape_func(search_bs)
75 |
76 | resp["response"] = (await manga_list)[:total_res]
77 | return resp
78 |
79 | @staticmethod
80 | async def __scrape_list(search_bs: BeautifulSoup) -> List[Dict[str, str]]:
81 |
82 | res = []
83 |
84 | book_list = search_bs.find("div", {"id": "book_list"})
85 |
86 | for title_bs in book_list.find_all("div", {"class": "item"}):
87 | manga = {}
88 | text_class = title_bs.find("div", {"class": "text"})
89 | manga["title"] = text_class.find('a').string
90 | total_chps = text_class.find('span').text.strip(" ").strip("- ").split()
91 | try:
92 | manga["total_chps"] = float(total_chps[0])
93 | except ValueError:
94 | manga["total_chps"] = float(total_chps[-1])
95 |
96 | manga["genres"] = []
97 | for genre in title_bs.find("div", {"class": "genres"}).find_all("a"):
98 | manga["genres"].append(genre.string)
99 |
100 | media = title_bs.find("div", {"class": "media"})
101 | media_a = media.find("div", {"class": "wrap_img"}).find("a")
102 | manga["cover"] = media_a.find("img")["src"]
103 | manga["status"] = media.find("div", {"class": "status"}).text.strip().capitalize()
104 |
105 | manga_session = media_a['href']
106 | manga["manga_detail"] = f"{ServerConfig.API_SERVER_ADDRESS}/manga_detail?session={manga_session}"
107 | manga["session"] = manga_session
108 |
109 | res.append(manga)
110 |
111 | return res
112 |
113 | @staticmethod
114 | async def __scrape_detail(search_bs: BeautifulSoup) -> List[Dict[str, str]]:
115 |
116 | manga = {}
117 |
118 | info = search_bs.find("div", {"class": "info"})
119 |
120 | manga["title"] = info.find("h1", {"class": "heading"}).string
121 | meta_data = info.find("ul", {"class": "meta d-table"})
122 | manga["total_chps"] = meta_data.find("div", {"class": "new_chap"}).text.strip(" ").split()[-1]
123 | manga["genres"] = []
124 | for genre in meta_data.find("div", {"class": "genres"}).find_all("a"):
125 | manga["genres"].append(genre.string)
126 |
127 | manga["cover"] = search_bs.find("div", {"class": "cover"}).find("img")["src"]
128 | manga["status"] = meta_data.find("div", {"class": "status"}).text.capitalize()
129 | manga_url = search_bs.find("meta", property="og:url")["content"]
130 | manga["manga_detail"] = f"{ServerConfig.API_SERVER_ADDRESS}/manga_detail?session={manga_url}"
131 | manga["session"] = manga_url
132 |
133 | return [manga]
134 |
135 | async def get_chp_session(self, manga_session: str) -> dict[str, list[Any] | dict[Any, Any] | str]:
136 | res = {"chapters": [], "description": {}}
137 |
138 | resp_text = await (await self.get(manga_session)).text()
139 |
140 | detail_bs = BeautifulSoup(resp_text, 'html.parser')
141 |
142 | for info in detail_bs.find("div", {"class": 'chapters'}).find_all("tr"):
143 | chp_info = info.find("div", {"class": "chapter"})
144 | _chp_info = chp_info.text.lstrip("Chapter ").split(":")
145 | chp_no, chp_name = _chp_info[0], _chp_info[-1]
146 | chp_session = chp_info.find("a")["href"]
147 | res["chapters"].append({chp_no: {
148 | "chp_link": f'{ServerConfig.API_SERVER_ADDRESS}/read?chp_session={chp_session}',
149 | "chp_name": chp_name,
150 | "chp_session": chp_session}})
151 |
152 | for meta_data in detail_bs.find("ul", {"class": "meta"}).find_all("div", {"class": ["alt_name", "authors"]}):
153 | if "alt_name" in meta_data["class"]:
154 | res["description"]["alt_name"] = meta_data.text
155 | else:
156 | res["description"]["author"] = meta_data.text
157 |
158 | res["description"]["summary"] = detail_bs.find("div", {"class": "summary"}).find("p").text
159 | res["recommendation"] = f"{ServerConfig.API_SERVER_ADDRESS}/recommendation?type=manga&manga_session={manga_session}"
160 |
161 | return res
162 |
163 | async def get_manga_source_data(self, chp_session: str) -> List[str]:
164 | return (await self.get_manifest_file(chp_session))[0]
165 |
166 | async def get_recommendation(self, manga_session: str) -> List[Dict[str, str]]:
167 | resp_text = await (await self.get(manga_session)).text()
168 |
169 | rec_bs = BeautifulSoup(resp_text, 'html.parser')
170 |
171 | recommendations = []
172 |
173 | for widget in rec_bs.find("div", {"id": "hot_book"}).find_all("div", {"class": "widget"}):
174 |
175 | # only add items from similar-series widget
176 | if widget.find("div", {"class": "widget-title"}).find("span").text.lower() == "similar series":
177 |
178 | for rec in widget.find_all("div", {"class": "item"}):
179 |
180 | recommendation = {"title": str, "total_eps": float, "cover": str, "status": str, "manga_detail": str} # noqa
181 |
182 | recommendation["cover"] = rec.find("div", {"class": "wrap_img"}).find("a")["href"]
183 | rec_data = rec.find("div", {"class": "text"})
184 | title_data = rec_data.find("h3")
185 | recommendation["title"] = title_data.text
186 | recommendation["total_eps"] = float(rec_data.find("div", {"class": "chapter"}).text.strip("Chapter ").split(" ")[0])
187 | recommendation["status"] = rec_data.find("div", {"class": "status"}).text
188 | manga_session = title_data.find('a')['href']
189 | recommendation["manga_detail"] = f"{ServerConfig.API_SERVER_ADDRESS}/manga_detail?session={manga_session}"
190 | recommendation["session"] = title_data.find('a')['href']
191 |
192 | recommendations.append(recommendation)
193 | break
194 |
195 | return recommendations
196 |
197 | async def get_manifest_file(self, chp_url) -> (List[str], '_', ("series_name", "file_name")):
198 | """
199 | This function will return all images from a particular chapter
200 | This func will call the self.get_manga_source_data
201 | It is called by downloadManager
202 | It is implemented as get_manifest_file to provide uniform interface
203 |
204 | """
205 | resp_text = await (await self.get(chp_url)).text()
206 | p = re.compile("var thzq=(.*);") # get all image links from variable inside the script tag
207 | m = p.search(resp_text)
208 | if m:
209 | chp_links = m.group(1).split(";")[0].strip("[]").replace("'", "").split(",")
210 | if chp_links[-1] == "":
211 | chp_links.pop()
212 | series_name = chp_url.split("/")[4].split(".")[0]
213 | file_name = BeautifulSoup(resp_text, 'html.parser').find("select", {"name": "chapter_select"}).find("option", {"selected": "selected"}).text
214 | return chp_links, None, [series_name, file_name]
215 | return [], None, "", ""
216 |
217 | async def get_links(self, manga_session: str, page: int = 1) -> List[str]:
218 | resp_text = await (await self.get(manga_session)).text()
219 | resp_bs = BeautifulSoup(resp_text, 'html.parser')
220 | res = []
221 | for tr in resp_bs.find("div", {"class": "chapters"}).find_all("tr"):
222 | res.append(tr.find("div", {"class": "chapter"}).find("a")["href"])
223 |
224 | return res
225 |
--------------------------------------------------------------------------------
/src/screens/inbuiltPlayerScreen.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {
4 | Box,
5 | Button,
6 | Center,
7 | Flex,
8 | Select,
9 | Stack,
10 | Text,
11 | Heading,
12 | Skeleton,
13 | } from "@chakra-ui/react";
14 | import { useEffect, useState } from "react";
15 | import { useSelector, useDispatch } from "react-redux";
16 |
17 | import { useNavigate } from "react-router-dom";
18 | import { BiArrowBack } from "react-icons/bi";
19 |
20 | import { addCurrentEp, addEpisodesDetails, getStreamDetails } from "../store/actions/animeActions";
21 | import VideoPlayer from "../components/video-player";
22 | import PaginateCard from "../components/paginateCard";
23 | import server from "../utils/axios";
24 |
25 | const InbuiltPlayerScreen = () => {
26 | const dispatch = useDispatch();
27 | const { details, loading: streamLoading } = useSelector((state) => state.animeStreamDetails);
28 | const navigate = useNavigate();
29 |
30 | const { animes: data, loading } = useSelector((state) => state.animeSearchList);
31 |
32 | const epDetails = useSelector((state) => state.animeCurrentEp);
33 | const urlDetails = useSelector((state) => state.animeEpUrl);
34 | const { details: anime } = useSelector((state) => state.animeDetails);
35 |
36 | const { details: eps_details, loading: eps_loading } = useSelector(
37 | (state) => state.animeEpisodesDetails
38 | );
39 |
40 | const [language, setLanguage] = useState("jpn");
41 | const [qualityOptions, setQualityOptions] = useState([]);
42 | const [test, setTest] = useState({});
43 | const [prevTime, setPrevTime] = useState(null);
44 | const [player, setPlayer] = useState(undefined);
45 | const [toogleRefresh, setToogleRefresh] = useState(Math.floor(Math.random() * 100000));
46 |
47 | const languageChangeHandler = (e) => {
48 | setPrevTime(player.currentTime());
49 | setLanguage(e.target.value);
50 | };
51 | let ep_no = parseInt(epDetails?.details?.current_ep);
52 |
53 | const pageChangeHandler = async (url) => {
54 | if (url) {
55 | const { data } = await server.get(url);
56 | dispatch(addEpisodesDetails({ ...data, current_ep: ep_no + 1 }));
57 | }
58 | };
59 | let current_page_eps = eps_details?.ep_details;
60 |
61 | const nextEpHandler = () => {
62 | setToogleRefresh(null);
63 |
64 | if (ep_no == Object.keys(current_page_eps[current_page_eps.length - 1])[0]) {
65 | if (eps_details.next_page_url) {
66 | pageChangeHandler(eps_details.next_page_url);
67 | } else {
68 | return;
69 | }
70 | }
71 |
72 | let item;
73 |
74 | current_page_eps.map((single_ep) => {
75 | if (Object.keys(single_ep)[0] == ep_no + 1) {
76 | console.log(single_ep);
77 | item = Object.values(single_ep)[0];
78 | }
79 | });
80 |
81 | if (item) {
82 | console.log("item", item);
83 | dispatch(getStreamDetails(item.stream_detail));
84 | dispatch(
85 | addCurrentEp({
86 | ...item,
87 | current_ep: ep_no + 1,
88 | })
89 | );
90 | console.log(item);
91 |
92 | setToogleRefresh(Math.floor(Math.random() * 100000));
93 | }
94 | };
95 | const prevEpHandler = () => {
96 | if (ep_no == Object.keys(current_page_eps[0])[0]) {
97 | if (eps_details.prev_page_url) {
98 | pageChangeHandler(eps_details.prev_page_url);
99 | } else {
100 | return;
101 | }
102 | }
103 |
104 | let item;
105 |
106 | current_page_eps.map((single_ep) => {
107 | if (Object.keys(single_ep)[0] == ep_no) {
108 | item = Object.values(single_ep)[0];
109 | }
110 | });
111 |
112 | if (item) {
113 | dispatch(getStreamDetails(item.stream_detail));
114 | dispatch(
115 | addCurrentEp({
116 | ...item,
117 | current_ep: ep_no - 1,
118 | })
119 | );
120 | }
121 | };
122 |
123 | useEffect(() => {
124 | if (!details || !player) return;
125 | if (!details[language]) return;
126 | player.src({
127 | src: details[language],
128 | type: "application/x-mpegURL",
129 | withCredentials: false,
130 | });
131 | player.poster("");
132 | }, [details, streamLoading]);
133 | console.log("streamLoading", streamLoading);
134 | console.log("player", player);
135 |
136 | useEffect(() => {
137 | if (window) {
138 | window?.scrollTo(0, 0);
139 | }
140 | }, []);
141 |
142 | return (
143 |
144 |
150 | {epDetails && anime && (
151 |
152 |
153 | navigate("/anime-details")}
155 | alignSelf={"flex-start"}
156 | _hover={{
157 | cursor: "pointer",
158 | }}
159 | display="flex"
160 | justifyContent={"center"}
161 | alignItems={"center"}
162 | mr={6}
163 | height={"fit-content"}
164 | mt={1}>
165 |
166 | Back
167 |
168 |
174 |
175 | {anime.jp_name ? `${anime.jp_name}` : ""}{" "}
176 | {anime.eng_name ? ` | ${anime.eng_name}` : ""}
177 | {anime.title ? `${anime.title}` : ""}
178 |
179 |
180 | {`| Episode ${epDetails?.details?.current_ep}`}
181 |
182 |
183 |
184 |
185 | {details && language && epDetails && !streamLoading ? (
186 |
197 | ) : (
198 |
199 | )}
200 |
201 | )}
202 |
203 |
212 |
219 |
222 |
240 |
243 |
244 |
245 |
255 |
256 |
257 |
258 | );
259 | };
260 |
261 | export default InbuiltPlayerScreen;
262 |
--------------------------------------------------------------------------------
/src/components/paginateCard.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {
4 | Box,
5 | Button,
6 | Fade,
7 | Flex,
8 | Menu,
9 | MenuButton,
10 | MenuDivider,
11 | MenuGroup,
12 | MenuItem,
13 | MenuList,
14 | Skeleton,
15 | Spacer,
16 | Text,
17 | useDisclosure,
18 | useToast,
19 | } from "@chakra-ui/react";
20 | import React, { useEffect, useState } from "react";
21 | import { useDispatch, useSelector } from "react-redux";
22 | import {
23 | addCurrentEp,
24 | addEpisodesDetails,
25 | getRecommendations,
26 | getStreamDetails,
27 | } from "../store/actions/animeActions";
28 | import { useNavigate } from "react-router-dom";
29 | import server from "../utils/axios";
30 | import { downloadVideo } from "../store/actions/downloadActions";
31 | import MetaDataPopup from "./metadata-popup";
32 |
33 | export default function PaginateCard({
34 | data,
35 | loading,
36 | ep_details,
37 | redirect,
38 | isSingleAvailable,
39 | qualityOptions,
40 | player,
41 | setTest,
42 | recommendationLoading,
43 | }) {
44 |
45 | const toast = useToast();
46 | const { isOpen, onOpen, onClose } = useDisclosure();
47 |
48 | const { loading: epsLoading } = useSelector((state) => state.animeEpisodesDetails);
49 | const epDetails = useSelector((state) => state.animeCurrentEp);
50 | const { session } = useSelector((state) => state.animeDetails.details);
51 | let currentEp = parseInt(epDetails?.details?.current_ep);
52 | const { loading: downloadLoading } = useSelector((state) => state.animeDownloadDetails);
53 | console.log(epsLoading);
54 | const [isDownloadButtonAvailable, setIsDownloadButtonAvailable] = useState(false);
55 | const navigate = useNavigate();
56 | const dispatch = useDispatch();
57 | const episodeClickHandler = (item, ep_no) => {
58 | setIsDownloadButtonAvailable(false);
59 | console.log(item);
60 | dispatch(getStreamDetails(item.stream_detail));
61 | dispatch(
62 | addCurrentEp({
63 | ...item,
64 | current_ep: ep_no,
65 | })
66 | );
67 | // sleep(2000).then(() => {
68 | // setIsDownloadButtonAvailable(true);
69 | // setTest({ assdfda: "assdfdasd" });
70 | // });
71 | // dispatch(getRecommendations(ep_details.recommendation));
72 | if (redirect) {
73 | navigate("/play");
74 | }
75 | };
76 | const pageChangeHandler = async (url) => {
77 | setIsDownloadButtonAvailable(false);
78 |
79 | const { data } = await server.get(url);
80 | dispatch(addEpisodesDetails(data));
81 |
82 | sleep(2000).then(() => {
83 | setIsDownloadButtonAvailable(true);
84 | });
85 | };
86 | let coloredIdx;
87 |
88 | console.log(ep_details);
89 |
90 | if (!loading && ep_details) {
91 | let current_page_eps = ep_details.ep_details;
92 | current_page_eps.map((single_ep, idx) => {
93 | if (Object.keys(single_ep)[0] == currentEp) {
94 | coloredIdx = idx;
95 | }
96 | });
97 | }
98 |
99 | useEffect(() => {
100 | if (downloadLoading) {
101 | onOpen();
102 | } else {
103 | onClose();
104 | }
105 | }, [downloadLoading]);
106 |
107 | const downloadPageHandler = async () => {
108 | dispatch(
109 | downloadVideo({
110 | anime_session: session,
111 | })
112 | );
113 | };
114 |
115 | const singleDownloadHandler = (url) => {
116 | dispatch(
117 | downloadVideo({
118 | manifest_url: url.slice(2),
119 | })
120 | );
121 | };
122 |
123 | useEffect(() => {
124 | if (!isSingleAvailable) return;
125 | setTest({ asda: "asdasd" });
126 |
127 | sleep(2000).then(() => {
128 | setIsDownloadButtonAvailable(true);
129 | setTest({ assdfda: "assdfdasd" });
130 | });
131 | }, [isSingleAvailable]);
132 | useEffect(() => {
133 | sleep(4000).then(() => {
134 | setIsDownloadButtonAvailable(true);
135 | });
136 | }, [isDownloadButtonAvailable]);
137 | function sleep(time) {
138 | return new Promise((resolve) => setTimeout(resolve, time));
139 | }
140 | return (
141 | <>
142 |
143 | {!loading && ep_details ? (
144 |
145 | {ep_details?.ep_details.map((item, key) => {
146 | return (
147 |
162 | episodeClickHandler(
163 | Object.values(item)[0],
164 | Object.keys(item)[0]
165 | )
166 | }>
167 | {Object.keys(item)[0]}
168 |
169 | );
170 | })}
171 |
172 | ) : (
173 |
174 | {Array(25)
175 | .fill(0)
176 | .map((item, key) => {
177 | return (
178 |
186 |
193 | {key + 1}
194 |
195 |
196 | );
197 | })}
198 |
199 | )}
200 | {/* */}
201 |
202 |
203 |
204 | {data && (
205 |
206 |
207 |
217 |
218 |
219 |
220 |
221 | {ep_details?.next_page_url && (
222 |
228 | )}
229 |
230 | )}
231 | {!isSingleAvailable && }
232 |
233 | {isSingleAvailable && (
234 | <>
235 |
236 |
237 |
259 | >
260 | )}{" "}
261 |
262 |
263 |
264 | >
265 | );
266 | };
--------------------------------------------------------------------------------
/src/screens/downloadsScreen.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {
4 | Box,
5 | Center,
6 | Flex,
7 | Heading,
8 | Stack,
9 | Table,
10 | TableContainer,
11 | Tbody,
12 | Td,
13 | Text,
14 | Th,
15 | Thead,
16 | Tr,
17 | useDisclosure,
18 | } from "@chakra-ui/react";
19 | import { useState, useEffect, useContext } from "react";
20 |
21 | import { useSelector, useDispatch } from "react-redux";
22 | import { TbMoodSad } from "react-icons/tb";
23 | import { FaPlay } from "react-icons/fa";
24 | import { AiOutlineFolderOpen } from "react-icons/ai";
25 |
26 | import {
27 | cancelLiveDownload,
28 | getDownloadHistory,
29 | pauseLiveDownload,
30 | resumeLiveDownload,
31 | } from "../store/actions/animeActions";
32 |
33 | import { formatBytes } from "../utils/formatBytes";
34 | import { SocketContext } from "../context/socket";
35 | import DownloadList from "../components/downloadList";
36 | import ExternalPlayerPopup from "../components/externalPopup";
37 |
38 | function sleep(milliseconds) {
39 | const date = Date.now();
40 | let currentDate = null;
41 | do {
42 | currentDate = Date.now();
43 | } while (currentDate - date < milliseconds);
44 | }
45 | const { shell } = window.require("electron");
46 |
47 | let openFileExplorer = (file_location) => {
48 | // Show a folder in the file manager
49 | // Or a file
50 | console.log(file_location);
51 |
52 | shell.showItemInFolder(file_location);
53 | };
54 |
55 | const DownloadScreen = () => {
56 | const dispatch = useDispatch();
57 | const [filesStatus, setFilesStatus] = useState({});
58 | const [test, setTest] = useState({});
59 | const [connected, setConnected] = useState(false);
60 | const [playId, setPlayId] = useState(null);
61 | const historyDetails = useSelector((state) => state.animeLibraryDetails);
62 | const client = useContext(SocketContext);
63 | const { isOpen, onOpen, onClose } = useDisclosure();
64 |
65 | useEffect(() => {
66 | dispatch(getDownloadHistory());
67 | if (!client) return;
68 | // client.onopen = () => {
69 | // console.log("WebSocket Client Connected");
70 | // client.send(JSON.stringify({ type: "connect" }));
71 | // setConnected(true);
72 | // setConnected(true);
73 | // };
74 |
75 | return () => {
76 | setConnected(false);
77 | };
78 | }, [client]);
79 |
80 | const onMessageListner = () => {
81 | client.onmessage = (message) => {
82 | let packet = JSON.parse(message.data);
83 | let { data } = packet;
84 |
85 | let tempData = data;
86 | setTest(data);
87 |
88 | setFilesStatus((prev) => {
89 | let temp = filesStatus;
90 |
91 | if (tempData.downloaded === tempData.total_size && tempData.total_size > 0) {
92 | if (!filesStatus) return {};
93 | const { [tempData.id]: removedProperty, ...restObject } = filesStatus;
94 |
95 | // sleep(5000);
96 |
97 | dispatch(getDownloadHistory());
98 |
99 | return restObject;
100 | } else {
101 | temp[tempData.id] = tempData;
102 | let sec = {};
103 | if (historyDetails?.details) {
104 | historyDetails?.details?.forEach((history_item) => {
105 | if (history_item.status !== "downloaded")
106 | sec[history_item.id] = history_item;
107 | });
108 | return { ...sec, ...temp };
109 | } else {
110 | return filesStatus;
111 | }
112 | }
113 |
114 | // console.log(temp);
115 | });
116 |
117 | // if (tempData.downloaded === tempData.total_size) {
118 | // console.time()
119 |
120 | // console.time()
121 |
122 | // }
123 | };
124 | };
125 |
126 | useEffect(() => {
127 | setFilesStatus(() => {
128 | let sec = {};
129 | historyDetails?.details?.forEach((history_item) => {
130 | if (history_item.status !== "downloaded") {
131 | sec[history_item.id] = history_item;
132 | }
133 | });
134 | return { ...filesStatus, ...sec };
135 | });
136 | }, [historyDetails]);
137 |
138 | useEffect(() => {
139 | if (!client) return;
140 | onMessageListner();
141 | }, [filesStatus]);
142 |
143 | const playClickHandler = async (id) => {
144 | setPlayId(id);
145 | onOpen();
146 | };
147 |
148 | const cancelDownloadHandler = (id) => {
149 | cancelLiveDownload(id);
150 |
151 | setFilesStatus((prev) => {
152 | try {
153 | if (!filesStatus) return {};
154 | const { [id]: removedProperty, ...restObject } = filesStatus;
155 |
156 | return restObject;
157 | } catch (error) {
158 | console.log(error);
159 | }
160 | });
161 | };
162 | const pauseDownloadHandler = (id) => {
163 | pauseLiveDownload(id);
164 | // sleep(5000);
165 | dispatch(getDownloadHistory());
166 | };
167 | const resumeDownloadHandler = (id) => {
168 | resumeLiveDownload(id);
169 | };
170 |
171 | return (
172 |
173 |
174 | {" "}
175 |
176 |
177 | Active Downloads
178 |
179 |
180 |
188 |
189 | {filesStatus && Object.entries(filesStatus).length === 0 ? (
190 |
191 |
196 | No Active Download
197 |
198 |
199 |
200 |
201 |
202 |
203 | ) : (
204 |
205 |
206 |
207 |
208 | |
209 | FILE NAME |
210 | STATUS |
211 | SPEED |
212 | SIZE |
213 | |
214 |
215 |
216 |
222 |
223 |
224 | )}
225 |
226 |
227 |
228 |
229 |
230 | History
231 |
232 |
240 |
241 |
242 |
243 |
244 | |
245 | FILE NAME |
246 | STATUS |
247 | TOTAL SIZE |
248 | CREATED ON |
249 | |
250 |
251 |
252 |
253 | {historyDetails?.details &&
254 | historyDetails?.details?.length !== 0 ? (
255 | historyDetails.details.map((history_item, idx) => {
256 | if (history_item.status === "downloaded") {
257 | return (
258 |
259 | |
260 | {" "}
261 |
264 | playClickHandler(
265 | history_item.id
266 | )
267 | }>
268 |
270 | openFileExplorer(
271 | history_item.file_location
272 | )
273 | }
274 | />
275 |
276 | |
277 | {history_item.file_name} |
278 | {history_item.status} |
279 |
280 | {" "}
281 | {formatBytes(history_item.total_size)}
282 | |
283 | {history_item.created_on} |
284 |
285 | {" "}
286 |
288 | openFileExplorer(
289 | history_item.file_location
290 | )
291 | }
292 | sx={{ cursor: "pointer" }}>
293 |
294 |
295 | |
296 |
297 | );
298 | } else {
299 | return null;
300 | }
301 | })
302 | ) : (
303 |
309 |
314 | No Previous Downloads
315 |
316 |
317 |
318 |
319 |
320 |
321 | )}
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
335 |
336 | );
337 | };
338 |
339 | export default DownloadScreen;
340 |
--------------------------------------------------------------------------------