├── 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 | logo 10 | 15 | 16 | 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 | 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 | ![Banner](src/assets/img/home_screen_logo.png) 4 | 5 | ![GitHub contributors](https://img.shields.io/github/contributors/cosmicoppai/LiSA?color=lightgrey) 6 | [![GitHub forks](https://img.shields.io/github/forks/cosmicoppai/LiSA?color=lightgrey)](https://github.com/Cosmicoppai/LiSA/network) 7 | [![GitHub stars](https://img.shields.io/github/stars/cosmicoppai/LiSA?color=lightgrey)](https://github.com/Cosmicoppai/LiSA/stargazers) 8 | [![GitHub issues](https://img.shields.io/github/issues/Cosmicoppai/LiSA?color=lightgrey)](https://github.com/Cosmicoppai/LiSA/issues) 9 | [![MIT License](https://img.shields.io/badge/license-MIT-lightgrey)](./LICENSE) 10 | 11 | ![image](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python&logoColor=blue) 12 | ![image](https://img.shields.io/badge/Electron-2B2E3A?style=for-the-badge&logo=electron&logoColor=9FEAF9) 13 | ![image](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) 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 | ![Annotation 2023-09-25 171308 - Copy](https://github.com/developerrahulofficial/jennie-anime-player/assets/83329806/4233126c-047a-45d9-a1a5-98bc2b2969bb) 131 | ![Annotation 2023-09-25 171502 - Copy](https://github.com/developerrahulofficial/jennie-anime-player/assets/83329806/d99a006d-8f15-4baa-94c4-c1178e852dc0) 132 | ![Annotation 2023-09-25 171419](https://github.com/developerrahulofficial/jennie-anime-player/assets/83329806/6469bfc9-b3d0-42ad-bf7d-c8b98d338f08) 133 | ![Annotation 2023-09-25 171355 - Copy](https://github.com/developerrahulofficial/jennie-anime-player/assets/83329806/d3a31367-097c-4eba-8224-c5c91b7821cb) 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 | logo{" "} 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 | not-found 135 | 136 | Anime Not Found 137 | 138 | 139 | The result you're looking for does not seem to exist 140 | 141 | 142 | )} 143 | {loading && loader} 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 | 238 | setTest({ sdf: "asdaasdsd" })}> 242 | Download 243 | 244 | 245 | 246 | {qualityOptions.map(({ id, height }) => { 247 | return ( 248 | singleDownloadHandler(id)} 250 | key={id}> 251 | {height}p 252 | 253 | ); 254 | })} 255 | 256 | {/* */} 257 | 258 | 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 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 222 |
FILE NAMESTATUSSPEEDSIZE
223 |
224 | )} 225 |
226 |
227 |
228 | 229 | 230 | History 231 | 232 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 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 | 277 | 278 | 279 | 283 | 284 | 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 |
FILE NAMESTATUSTOTAL SIZECREATED ON
260 | {" "} 261 | 264 | playClickHandler( 265 | history_item.id 266 | ) 267 | }> 268 | 270 | openFileExplorer( 271 | history_item.file_location 272 | ) 273 | } 274 | /> 275 | 276 | {history_item.file_name} {history_item.status} 280 | {" "} 281 | {formatBytes(history_item.total_size)} 282 | {history_item.created_on} 285 | {" "} 286 | 288 | openFileExplorer( 289 | history_item.file_location 290 | ) 291 | } 292 | sx={{ cursor: "pointer" }}> 293 | 294 | 295 |
324 |
325 |
326 |
327 |
328 | 329 | 335 |
336 | ); 337 | }; 338 | 339 | export default DownloadScreen; 340 | --------------------------------------------------------------------------------