mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-21 06:31:58 +00:00
remove 44 commits across 4 months to remove shit like secrets, api keys etc from private groups and trackers
This commit is contained in:
parent
477a88418c
commit
8fb0bf4117
19 changed files with 506 additions and 84979 deletions
|
|
@ -1,3 +1,11 @@
|
||||||
|
# THIS IS AN ARCHIVE BRANCH, IT WAS PRIVATE FOR A REASON
|
||||||
|
|
||||||
|
This branch is entirely for the sake of "I worked on it, I want the commits", it was private because the code was dogshit and honestly a joke, and it's being re-written from scratch for a good reason!!!
|
||||||
|
This includes meme commits, testing on production, write your own JS framework and other blantant "don't do you fucking idiot", read the code/commits at your own risk!
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com) [](https://forthebadge.com)
|
[](https://forthebadge.com) [](https://forthebadge.com) [](https://forthebadge.com)
|
||||||
# Miru
|
# Miru
|
||||||
Miru is a simple anime torrent streaming client targetted at normies, meant as a replacement for shitty streaming sites with a lot of extra added functionality.
|
Miru is a simple anime torrent streaming client targetted at normies, meant as a replacement for shitty streaming sites with a lot of extra added functionality.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,18 @@ body {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noscript {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background: #000;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gsignin {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-color {
|
.badge-color {
|
||||||
background-color: var(--color) !important;
|
background-color: var(--color) !important;
|
||||||
border-color: var(--color) !important;
|
border-color: var(--color) !important;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
|
||||||
<meta property="og:title" content="Miru">
|
<meta property="og:title" content="Miru">
|
||||||
<meta property="og:url" content="">
|
<meta property="og:url" content="https://miru.pages.dev/">
|
||||||
<meta property="og:description" content="Miru - Torrent streaming made simple!">
|
<meta property="og:description" content="Miru - Torrent streaming made simple!">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<meta property="og:image" content="logo.png">
|
<meta property="og:image" content="logo.png">
|
||||||
|
|
@ -26,6 +26,16 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars" data-sidebar-shortcut-enabled="true">
|
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars" data-sidebar-shortcut-enabled="true">
|
||||||
|
<noscript>
|
||||||
|
<div class="d-flex align-items-center justify-content-center h-full w-full font-size-30 font-weight-bold">
|
||||||
|
What are you fucking stupid? Enable JavaScript.
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="gsignin" class="w-full h-full z-50 position-absolute d-none">
|
||||||
|
<div class="w-full h-full d-flex align-items-center justify-content-center font-size-30 font-weight-bold text-center">
|
||||||
|
Click to sign in with your Google Drive account.<br> Make sure you grant GDrive access!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal modal-full" id="viewAnime" tabindex="-1" role="dialog">
|
<div class="modal modal-full" id="viewAnime" tabindex="-1" role="dialog">
|
||||||
<div class="h-full modal-content bg-very-dark p-0 overflow-y-auto">
|
<div class="h-full modal-content bg-very-dark p-0 overflow-y-auto">
|
||||||
<button class="close pointer z-30 bg-dark shadow-lg border" data-dismiss="modal" type="button" aria-label="Close">
|
<button class="close pointer z-30 bg-dark shadow-lg border" data-dismiss="modal" type="button" aria-label="Close">
|
||||||
|
|
@ -155,16 +165,16 @@
|
||||||
<h1 class="title font-weight-bold text-white">Sypnosis</h1>
|
<h1 class="title font-weight-bold text-white">Sypnosis</h1>
|
||||||
<div class="font-size-16 pr-15">
|
<div class="font-size-16 pr-15">
|
||||||
</div>
|
</div>
|
||||||
<h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
|
<!-- <h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
|
||||||
<div class="d-flex flex-wrap justify-content-start">
|
<div class="d-flex flex-wrap justify-content-start"> -->
|
||||||
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
|
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
|
||||||
<img loading="lazy"
|
<img loading="lazy"
|
||||||
src="https://img1.ak.crunchyroll.com/i/spire1-tmb/b199406edeebc19a7f4e4412d6e1dfcc1365964779_full.jpg"
|
src="https://img1.ak.crunchyroll.com/i/spire1-tmb/b199406edeebc19a7f4e4412d6e1dfcc1365964779_full.jpg"
|
||||||
class="w-full h-full">
|
class="w-full h-full">
|
||||||
<div class="position-absolute ep-title w-full p-5 text-truncate bottom-0">Episode 1 - To You, 2,000
|
<div class="position-absolute ep-title w-full p-5 text-truncate bottom-0">Episode 1 - To You, 2,000
|
||||||
Years in the Future -The Fall of Zhiganshina (1)</div>
|
Years in the Future -The Fall of Zhiganshina (1)</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 px-sm-0 px-20">
|
<div class="col-md-3 px-sm-0 px-20">
|
||||||
<h1 class="title font-weight-bold text-white">Details</h1>
|
<h1 class="title font-weight-bold text-white">Details</h1>
|
||||||
|
|
@ -896,12 +906,8 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
||||||
<input id="subtitle1" type="text" list="subtitle1list" class="form-control form-control-lg"
|
<input id="subtitle1" type="text" list="subtitle1list" class="form-control form-control-lg"
|
||||||
autocomplete="off" value="SubsPlease">
|
autocomplete="off" value="SubsPlease">
|
||||||
<datalist id="subtitle1list">
|
<datalist id="subtitle1list">
|
||||||
<option value="SubsPlease">Roboto
|
<option value="SubsPlease">Roboto Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1</option>
|
||||||
Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1
|
<option value="Erai-raws">Open Sans Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1</option>
|
||||||
</option>
|
|
||||||
<option value="Erai-raws">Open Sans
|
|
||||||
Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1
|
|
||||||
</option>
|
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group w-300 mb-10 form-control-lg" data-toggle="tooltip" data-placement="top"
|
<div class="input-group w-300 mb-10 form-control-lg" data-toggle="tooltip" data-placement="top"
|
||||||
|
|
@ -942,7 +948,7 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
||||||
<label for="player6">Autoplay Next Episode</label>
|
<label for="player6">Autoplay Next Episode</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-switch mb-10 pl-10 font-size-16 w-300" data-toggle="tooltip" data-placement="top"
|
<div class="custom-switch mb-10 pl-10 font-size-16 w-300" data-toggle="tooltip" data-placement="top"
|
||||||
data-title="Pauses/Resumes Video Playback When Tabbing In/Out Of The App.">
|
data-title="Pauses/Resumes Video Playback When Tabbing In/Out Of The App">
|
||||||
<input type="checkbox" id="player10" checked>
|
<input type="checkbox" id="player10" checked>
|
||||||
<label for="player10">Pause When Tabbing Out</label>
|
<label for="player10">Pause When Tabbing Out</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -961,8 +967,8 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
||||||
<input id="torrent4" type="text" list="torrent4list" class="form-control form-control-lg"
|
<input id="torrent4" type="text" list="torrent4list" class="form-control form-control-lg"
|
||||||
autocomplete="off" value="SubsPlease">
|
autocomplete="off" value="SubsPlease">
|
||||||
<datalist id="torrent4list">
|
<datalist id="torrent4list">
|
||||||
<option value="SubsPlease">https://subsplease.org/rss/?r=</option>
|
<option value="SubsPlease">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=subsplease&q=</option>
|
||||||
<option value="Erai-raws">https://www.erai-raws.info/rss-</option>
|
<option value="Erai-raws">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=Erai-raws&q=</option>
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mb-10 w-300 form-control-lg" data-toggle="tooltip" data-placement="top"
|
<div class="input-group mb-10 w-300 form-control-lg" data-toggle="tooltip" data-placement="top"
|
||||||
|
|
@ -971,9 +977,9 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
||||||
<span class="input-group-text w-100 justify-content-center">Quality</span>
|
<span class="input-group-text w-100 justify-content-center">Quality</span>
|
||||||
</div>
|
</div>
|
||||||
<select class="form-control form-control-lg" id="torrent1">
|
<select class="form-control form-control-lg" id="torrent1">
|
||||||
<option value="1080" selected>1080p</option>
|
<option value='"1080"' selected>1080p</option>
|
||||||
<option value="720">720p</option>
|
<option value='"720"'>720p</option>
|
||||||
<option value='480"||"540'>SD</option>
|
<option value='"480"||"540"'>SD</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<break></break>
|
<break></break>
|
||||||
|
|
@ -998,6 +1004,11 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
||||||
<input type="checkbox" id="torrent9">
|
<input type="checkbox" id="torrent9">
|
||||||
<label for="torrent9">Batch Lookup</label>
|
<label for="torrent9">Batch Lookup</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="custom-switch mb-10 pl-10 font-size-16 w-300" data-toggle="tooltip" data-placement="top"
|
||||||
|
data-title="Shows All Anime, Even Ones That Can't Be Played Back">
|
||||||
|
<input type="checkbox" id="other3">
|
||||||
|
<label for="other2">Show All Anime</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content my-10">
|
<div class="content my-10">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { alID } from './interface.js'
|
import { alID } from './interface.js'
|
||||||
|
import { settings } from './settings.js'
|
||||||
import halfmoon from 'halfmoon'
|
import halfmoon from 'halfmoon'
|
||||||
|
|
||||||
async function handleRequest (opts) {
|
async function handleRequest (opts) {
|
||||||
|
|
@ -71,7 +72,8 @@ export async function alRequest (opts) {
|
||||||
perPage: opts.perPage || 30,
|
perPage: opts.perPage || 30,
|
||||||
status_in: opts.status_in || '[CURRENT,PLANNING]',
|
status_in: opts.status_in || '[CURRENT,PLANNING]',
|
||||||
chunk: opts.chunk || 1,
|
chunk: opts.chunk || 1,
|
||||||
perchunk: opts.perChunk || 30
|
perchunk: opts.perChunk || 30,
|
||||||
|
startDate: (!settings.other3 && (opts.startDate || 20210328)) || 10000000
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -260,12 +262,12 @@ query ($page: Int, $perPage: Int, $from: Int, $to: Int) {
|
||||||
variables.status = opts.status
|
variables.status = opts.status
|
||||||
variables.sort = opts.sort || 'SEARCH_MATCH'
|
variables.sort = opts.sort || 'SEARCH_MATCH'
|
||||||
query = `
|
query = `
|
||||||
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat) {
|
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat, $startDate: FuzzyDateInt) {
|
||||||
Page (page: $page, perPage: $perPage) {
|
Page (page: $page, perPage: $perPage) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
},
|
},
|
||||||
media(type: $type, search: $search, sort: $sort, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format) {
|
media(type: $type, search: $search, sort: $sort, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format, startDate_greater: $startDate) {
|
||||||
${queryObjects}
|
${queryObjects}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,14 +111,8 @@ export async function resolveFileMedia (opts) {
|
||||||
async function resolveTitle (title) {
|
async function resolveTitle (title) {
|
||||||
if (!(title in relations)) {
|
if (!(title in relations)) {
|
||||||
// resolve name and shit
|
// resolve name and shit
|
||||||
let method, res
|
const method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH', startDate: 10000000 }
|
||||||
if (opts.isRelease) {
|
let res = await alRequest(method)
|
||||||
method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING'], sort: 'SEARCH_MATCH' }
|
|
||||||
// maybe releases should include this and last season? idfk
|
|
||||||
} else {
|
|
||||||
method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH' }
|
|
||||||
}
|
|
||||||
res = await alRequest(method)
|
|
||||||
if (!res.data.Page.media[0]) {
|
if (!res.data.Page.media[0]) {
|
||||||
const index = method.name.search(/S\d/)
|
const index = method.name.search(/S\d/)
|
||||||
method.name = ((index !== -1 && method.name.slice(0, index) + method.name.slice(index + 1, method.name.length)) || method.name).replace('(TV)', '').replace(/ (19[5-9]\d|20[0-6]\d)/, '').replace('-', '')
|
method.name = ((index !== -1 && method.name.slice(0, index) + method.name.slice(index + 1, method.name.length)) || method.name).replace('(TV)', '').replace(/ (19[5-9]\d|20[0-6]\d)/, '').replace('-', '')
|
||||||
|
|
@ -138,7 +132,11 @@ export async function resolveFileMedia (opts) {
|
||||||
const parseObjs = await Promise.all(parsePromises)
|
const parseObjs = await Promise.all(parsePromises)
|
||||||
await Promise.all([...new Set(parseObjs.map(obj => obj.anime_title))].map(title => resolveTitle(title)))
|
await Promise.all([...new Set(parseObjs.map(obj => obj.anime_title))].map(title => resolveTitle(title)))
|
||||||
const assoc = {}
|
const assoc = {}
|
||||||
for (const media of (await alRequest({ method: 'SearchIDS', id: [...new Set(parseObjs.map(obj => relations[obj.anime_title]))], perPage: 50 })).data.Page.media) assoc[media.id] = media
|
for (let ids = [...new Set(parseObjs.map(obj => relations[obj.anime_title]))]; ids.length; ids = ids.slice(50)) {
|
||||||
|
for await (const media of (await alRequest({ method: 'SearchIDS', id: ids.slice(0, 50), perPage: 50 })).data.Page.media) {
|
||||||
|
assoc[media.id] = media
|
||||||
|
}
|
||||||
|
}
|
||||||
const fileMedias = []
|
const fileMedias = []
|
||||||
for (const praseObj of parseObjs) {
|
for (const praseObj of parseObjs) {
|
||||||
let episode
|
let episode
|
||||||
|
|
@ -161,16 +159,16 @@ export async function resolveFileMedia (opts) {
|
||||||
tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node
|
tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node
|
||||||
increment = true
|
increment = true
|
||||||
}
|
}
|
||||||
if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) > (media.nextAiringEpisode?.episode || media.episodes)) {
|
if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) > (media.nextAiringEpisode?.episode || media.episodes)) {
|
||||||
// episode is still out of bounds
|
// episode is still out of bounds
|
||||||
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
||||||
await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment })
|
await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment })
|
||||||
} else if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) <= (media.nextAiringEpisode?.episode || media.episodes) && epMin - (opts.offset + tempMedia.episodes) > 0) {
|
} else if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) <= (media.nextAiringEpisode?.episode || media.episodes) && epMin - (opts.offset + media.episodes) > 0) {
|
||||||
// episode is in range, seems good! overwriting media to count up "seasons"
|
// episode is in range, seems good! overwriting media to count up "seasons"
|
||||||
if (opts.episode.constructor === Array) {
|
if (opts.episode.constructor === Array) {
|
||||||
episode = `${praseObj.episode_number[0] - (opts.offset + tempMedia.episodes)} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1] - (opts.offset + tempMedia.episodes)}`
|
episode = `${praseObj.episode_number[0] - (opts.offset + media.episodes)} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1] - (opts.offset + media.episodes)}`
|
||||||
} else {
|
} else {
|
||||||
episode = opts.episode - (opts.offset + tempMedia.episodes)
|
episode = opts.episode - (opts.offset + media.episodes)
|
||||||
}
|
}
|
||||||
if (opts.increment || increment) {
|
if (opts.increment || increment) {
|
||||||
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
||||||
|
|
|
||||||
84921
app/js/bundle.js
84921
app/js/bundle.js
File diff suppressed because one or more lines are too long
76
app/js/bundle.js.LICENSE.txt
Normal file
76
app/js/bundle.js.LICENSE.txt
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* range-parser
|
||||||
|
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||||
|
* Copyright(c) 2015-2016 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! bittorrent-protocol. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! blob-to-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! cache-chunk-store. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! create-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! https://mths.be/punycode v1.3.2 by @mathias */
|
||||||
|
|
||||||
|
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! immediate-chunk-store. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! lt_donthave. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! magnet-uri. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! mediasource. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! multistream. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! parse-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! render-media. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! run-parallel-limit. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! run-parallel. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! simple-concat. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! simple-get. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! simple-websocket. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! stream-to-blob-url. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! stream-to-blob. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! stream-with-known-length-to-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! torrent-discovery. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! torrent-piece. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! ut_metadata. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
|
|
||||||
|
/*! webtorrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@ import { resolveFileMedia, relations, nyaaSearch } from './anime.js'
|
||||||
import { getRSSurl, getRSSContent } from './rss.js'
|
import { getRSSurl, getRSSContent } from './rss.js'
|
||||||
import { settings } from './settings.js'
|
import { settings } from './settings.js'
|
||||||
import { client } from './main.js'
|
import { client } from './main.js'
|
||||||
import { countdown, flattenObj } from './util.js'
|
import { countdown, flattenObj, userBrowser } from './util.js'
|
||||||
import halfmoon from 'halfmoon'
|
import halfmoon from 'halfmoon'
|
||||||
export function loadHomePage () {
|
export function loadHomePage () {
|
||||||
const homeLoadElements = [navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore]
|
const homeLoadElements = [navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore]
|
||||||
|
|
@ -19,7 +19,7 @@ export function loadHomePage () {
|
||||||
if (!page) gallerySkeleton(browseGallery)
|
if (!page) gallerySkeleton(browseGallery)
|
||||||
const res = await alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, page: page || 1 })
|
const res = await alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, page: page || 1 })
|
||||||
const mediaList = res.data.Page.mediaList.map(i => i.media).filter(media => {
|
const mediaList = res.data.Page.mediaList.map(i => i.media).filter(media => {
|
||||||
return !(media.status === 'RELEASING' && media.mediaListEntry?.progress === media.nextAiringEpisode?.episode - 1)
|
return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
|
||||||
})
|
})
|
||||||
galleryAppend({ media: mediaList, gallery: browseGallery, method: 'continue', page: page || 1 })
|
galleryAppend({ media: mediaList, gallery: browseGallery, method: 'continue', page: page || 1 })
|
||||||
return res.data.Page.pageInfo.hasNextPage
|
return res.data.Page.pageInfo.hasNextPage
|
||||||
|
|
@ -94,7 +94,7 @@ export function loadHomePage () {
|
||||||
continue: function () {
|
continue: function () {
|
||||||
alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, perPage: 50 }).then(res => {
|
alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, perPage: 50 }).then(res => {
|
||||||
const mediaList = res.data.Page.mediaList.filter(({ media }) => {
|
const mediaList = res.data.Page.mediaList.filter(({ media }) => {
|
||||||
return !(media.status === 'RELEASING' && media.mediaListEntry?.progress === media.nextAiringEpisode?.episode - 1)
|
return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
|
||||||
}).slice(0, 5).map(i => i.media)
|
}).slice(0, 5).map(i => i.media)
|
||||||
galleryAppend({ media: mediaList, gallery: homeContinue })
|
galleryAppend({ media: mediaList, gallery: homeContinue })
|
||||||
})
|
})
|
||||||
|
|
@ -305,6 +305,7 @@ function genreBadges (genres = []) {
|
||||||
return badges
|
return badges
|
||||||
}
|
}
|
||||||
const detailsMap = [
|
const detailsMap = [
|
||||||
|
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
||||||
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
||||||
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
|
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
|
||||||
{ property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' },
|
{ property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' },
|
||||||
|
|
@ -316,7 +317,7 @@ const detailsMap = [
|
||||||
{ property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' },
|
{ property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' },
|
||||||
{ property: 'english', label: 'English', icon: 'title' },
|
{ property: 'english', label: 'English', icon: 'title' },
|
||||||
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
||||||
{ property: 'native', label: 'Native', icon: '日本', custom: 'icon' }
|
{ property: 'native', label: 'Native', icon: '語', custom: 'icon' }
|
||||||
]
|
]
|
||||||
/* global detailTemplate */
|
/* global detailTemplate */
|
||||||
const detailTemp = detailTemplate.cloneNode(true).content
|
const detailTemp = detailTemplate.cloneNode(true).content
|
||||||
|
|
@ -358,6 +359,8 @@ function mediaDetails (media) {
|
||||||
nodes[4].textContent = media.averageScore + '%'
|
nodes[4].textContent = media.averageScore + '%'
|
||||||
} else if (detail.property === 'season') {
|
} else if (detail.property === 'season') {
|
||||||
nodes[4].textContent = [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ')
|
nodes[4].textContent = [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ')
|
||||||
|
} else if (detail.property === 'episode') {
|
||||||
|
nodes[4].textContent = `Ep ${media.episode}: ${countdown(media.timeUntilAiring)}`
|
||||||
} else {
|
} else {
|
||||||
nodes[4].textContent = media[detail.property]
|
nodes[4].textContent = media[detail.property]
|
||||||
}
|
}
|
||||||
|
|
@ -425,8 +428,8 @@ export function viewMedia (input, episode) {
|
||||||
|
|
||||||
viewNodes[63].innerHTML = media.description
|
viewNodes[63].innerHTML = media.description
|
||||||
|
|
||||||
viewNodes[68].textContent = ''
|
viewNodes[66].textContent = ''
|
||||||
viewNodes[68].append(...mediaDetails(media))
|
viewNodes[66].append(...mediaDetails(media))
|
||||||
}
|
}
|
||||||
|
|
||||||
export let alID // login icon
|
export let alID // login icon
|
||||||
|
|
@ -454,3 +457,12 @@ export function initMenu () {
|
||||||
home.classList.add('noauth')
|
home.classList.add('noauth')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userBrowser === 'firefox') {
|
||||||
|
halfmoon.initStickyAlert({
|
||||||
|
content: 'Your browser will likely not support many video formats, containers and features. Please use Chromium!',
|
||||||
|
title: 'Bad Browser',
|
||||||
|
alertType: 'alert-danger',
|
||||||
|
fillType: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
6
app/js/keys.js
Normal file
6
app/js/keys.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const keys = {
|
||||||
|
apiKey: '',
|
||||||
|
clientId: '',
|
||||||
|
scope: 'https://www.googleapis.com/auth/drive.readonly',
|
||||||
|
discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { alEntry } from './anilist.js'
|
||||||
import { settings } from './settings.js'
|
import { settings } from './settings.js'
|
||||||
import { cardCreator, initMenu } from './interface.js'
|
import { cardCreator, initMenu } from './interface.js'
|
||||||
import halfmoon from 'halfmoon'
|
import halfmoon from 'halfmoon'
|
||||||
|
import { GDHandleTorrent } from './webseed.js'
|
||||||
|
|
||||||
const playerControls = {}
|
const playerControls = {}
|
||||||
for (const item of document.getElementsByClassName('ctrl')) {
|
for (const item of document.getElementsByClassName('ctrl')) {
|
||||||
|
|
@ -16,6 +17,9 @@ for (const item of document.getElementsByClassName('ctrl')) {
|
||||||
playerControls[item.dataset.name] = [playerControls[item.dataset.name], item]
|
playerControls[item.dataset.name] = [playerControls[item.dataset.name], item]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
client.destroy()
|
||||||
|
})
|
||||||
export const client = new WebTorrentPlayer({
|
export const client = new WebTorrentPlayer({
|
||||||
WebTorrentOpts: {
|
WebTorrentOpts: {
|
||||||
maxConns: 127,
|
maxConns: 127,
|
||||||
|
|
@ -23,7 +27,8 @@ export const client = new WebTorrentPlayer({
|
||||||
uploadLimit: settings.torrent7 * 1572864,
|
uploadLimit: settings.torrent7 * 1572864,
|
||||||
tracker: {
|
tracker: {
|
||||||
announce: settings.torrent10.split('\n')
|
announce: settings.torrent10.split('\n')
|
||||||
}
|
},
|
||||||
|
destroyStore: true
|
||||||
},
|
},
|
||||||
controls: playerControls,
|
controls: playerControls,
|
||||||
video: video,
|
video: video,
|
||||||
|
|
@ -48,6 +53,7 @@ client.on('download-done', ({ file }) => {
|
||||||
fillType: ''
|
fillType: ''
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
client.on('torrent', GDHandleTorrent)
|
||||||
client.on('watched', ({ filemedia }) => {
|
client.on('watched', ({ filemedia }) => {
|
||||||
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
|
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
|
||||||
if (settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
|
if (settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
|
||||||
|
|
@ -135,8 +141,6 @@ client.nowPlaying = { name: 'Miru' }
|
||||||
|
|
||||||
window.client = client
|
window.client = client
|
||||||
|
|
||||||
window.onbeforeunload = () => { return '' }
|
|
||||||
|
|
||||||
if (searchParams.get('file')) client.playTorrent(searchParams.get('file'))
|
if (searchParams.get('file')) client.playTorrent(searchParams.get('file'))
|
||||||
|
|
||||||
queueMicrotask(initMenu)
|
queueMicrotask(initMenu)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export async function nyaaRss (media, episode, isOffline) {
|
||||||
const titles = [...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join(')|(').replace(/&/g, '%26')
|
const titles = [...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join(')|(').replace(/&/g, '%26')
|
||||||
const ep = (media.episodes !== 1 && ((media.status === 'FINISHED' && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"Batch"|"Complete"|"+${episode}+"|"+${episode}v"|"S01"` : `"+${episode}+"|"+${episode}v"`)) || ''
|
const ep = (media.episodes !== 1 && ((media.status === 'FINISHED' && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"Batch"|"Complete"|"+${episode}+"|"+${episode}v"|"S01"` : `"+${episode}+"|"+${episode}v"`)) || ''
|
||||||
const excl = exclusions[userBrowser].join('|')
|
const excl = exclusions[userBrowser].join('|')
|
||||||
const quality = `"${settings.torrent1}"` || '"1080p"'
|
const quality = `${settings.torrent1}` || '"1080p"'
|
||||||
const trusted = settings.torrent3 === true ? 2 : 0
|
const trusted = settings.torrent3 === true ? 2 : 0
|
||||||
const url = new URL(`https://meowinjapanese.cf/?page=rss&c=1_2&f=${trusted}&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
|
const url = new URL(`https://meowinjapanese.cf/?page=rss&c=1_2&f=${trusted}&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
|
||||||
|
|
||||||
|
|
@ -101,8 +101,8 @@ export async function nyaaRss (media, episode, isOffline) {
|
||||||
|
|
||||||
export function getRSSurl () {
|
export function getRSSurl () {
|
||||||
if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) {
|
if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) {
|
||||||
return settings.torrent4 === 'Erai-raws' ? new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1 + '-magnet') : new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1)
|
return new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].textContent + settings.torrent1)
|
||||||
} else {
|
} else {
|
||||||
return settings.torrent4 + settings.torrent1 // add custom RSS
|
return new URL(settings.torrent4 + settings.torrent1) // add custom RSS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* global volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent5label, torrent7, torrent8, torrent9, torrent10, other1, other2, setRes, settingsTab, regProtButton, clearRelCache */
|
/* global volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent5label, torrent7, torrent8, torrent9, torrent10, other1, other2, other3, setRes, settingsTab, regProtButton, clearRelCache */
|
||||||
import { get, set, createStore } from 'idb-keyval'
|
import { get, set, createStore } from 'idb-keyval'
|
||||||
export const settingsElements = [
|
export const settingsElements = [
|
||||||
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent7, torrent8, torrent9, torrent10, other1, other2
|
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent7, torrent8, torrent9, torrent10, other1, other2, other3
|
||||||
]
|
]
|
||||||
setRes.addEventListener('click', restoreDefaults)
|
setRes.addEventListener('click', restoreDefaults)
|
||||||
settingsTab.addEventListener('click', applySettingsTimeout)
|
settingsTab.addEventListener('click', applySettingsTimeout)
|
||||||
|
|
@ -39,10 +39,6 @@ async function saveSettings () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(settings).length !== settingsElements.length + 1) {
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
let applyTimeout
|
let applyTimeout
|
||||||
function applySettingsTimeout () {
|
function applySettingsTimeout () {
|
||||||
clearTimeout(applyTimeout)
|
clearTimeout(applyTimeout)
|
||||||
|
|
@ -74,4 +70,6 @@ for (const setting of Object.entries(settings)) {
|
||||||
if (settingElement) settingElement.type === 'checkbox' ? settingElement.checked = setting[1] : settingElement.value = setting[1]
|
if (settingElement) settingElement.type === 'checkbox' ? settingElement.checked = setting[1] : settingElement.value = setting[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
other1.oninput = () => other1.checked && Notification.requestPermission().then(perm => { perm === 'denied' ? other1.checked = false : other1.checked = true })
|
other1.oninput = () => other1.checked && Notification.requestPermission().then(perm => { perm === 'denied' ? other1.checked = false : other1.checked = true })
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
import halfmoon from 'halfmoon'
|
import halfmoon from 'halfmoon'
|
||||||
|
|
||||||
halfmoon.showModal = id => {
|
halfmoon.showModal = id => {
|
||||||
const t = document.getElementById(id)
|
const t = document.getElementById(id)
|
||||||
t && t.classList.add('show')
|
t && t.classList.add('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
halfmoon.hideModal = id => {
|
halfmoon.hideModal = id => {
|
||||||
const t = document.getElementById(id)
|
const t = document.getElementById(id)
|
||||||
t && t.classList.remove('show')
|
t && t.classList.remove('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchParams = new URLSearchParams(location.href)
|
export const searchParams = new URLSearchParams(location.href)
|
||||||
if (searchParams.get('access_token')) {
|
if (searchParams.get('access_token')) {
|
||||||
localStorage.setItem('ALtoken', searchParams.get('access_token'))
|
localStorage.setItem('ALtoken', searchParams.get('access_token'))
|
||||||
window.location = '/app/#home'
|
window.location = '/app/#home'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userBrowser = (() => {
|
export const userBrowser = (() => {
|
||||||
if (window.chrome) {
|
if (window.chrome) {
|
||||||
return (navigator.userAgent.indexOf('Edg') !== -1) ? 'edge' : 'chromium'
|
return (navigator.userAgent.indexOf('Edg') !== -1) ? 'edge' : 'chromium'
|
||||||
}
|
}
|
||||||
return 'firefox'
|
return 'firefox'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
export function countdown (s) {
|
export function countdown (s) {
|
||||||
const d = Math.floor(s / (3600 * 24))
|
const d = Math.floor(s / (3600 * 24))
|
||||||
s -= d * 3600 * 24
|
s -= d * 3600 * 24
|
||||||
|
|
@ -50,3 +55,67 @@ export function flattenObj (obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser())
|
export const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser())
|
||||||
|
|
||||||
|
export function concat (chunks, size) {
|
||||||
|
if (!size) {
|
||||||
|
size = 0
|
||||||
|
let i = chunks.length || chunks.byteLength || 0
|
||||||
|
while (i--) size += chunks[i].length
|
||||||
|
}
|
||||||
|
const b = new Uint8Array(size)
|
||||||
|
let offset = 0
|
||||||
|
for (let i = 0, l = chunks.length; i < l; i++) {
|
||||||
|
const chunk = chunks[i]
|
||||||
|
b.set(chunk, offset)
|
||||||
|
offset += chunk.byteLength || chunk.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sleep = t => new Promise(resolve => setTimeout(resolve, t))
|
||||||
|
|
||||||
|
export class Queue {
|
||||||
|
constructor () {
|
||||||
|
this.queue = []
|
||||||
|
this.destroyed = false
|
||||||
|
this.lastfn = null
|
||||||
|
}
|
||||||
|
|
||||||
|
add (obj) { // index, fn
|
||||||
|
if (this.destroyed) return
|
||||||
|
// most common case, requests are in order
|
||||||
|
// also push to the end of queue if there's an outstanding high range request [for example EOF metadata]
|
||||||
|
// this impacts backwards seeking performance a bit, but is needed for metadata
|
||||||
|
if (!this.queue.length || obj.index > this.queue[this.queue.length - 1].index || obj.index < this.queue[0].index - 10) {
|
||||||
|
this.queue.push(obj)
|
||||||
|
if (this.queue.length === 1) this._next()
|
||||||
|
} else {
|
||||||
|
// otherwise if one request failed its likely the oldest, or older one, so iterate backwards [forwards since queue is reversed]
|
||||||
|
for (let i = 0; i < this.queue.length; i++) {
|
||||||
|
if (this.queue[i].index > obj.index) {
|
||||||
|
this.queue.splice(i, 0, obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.queue.push(obj)
|
||||||
|
console.warn('got bad')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _next () {
|
||||||
|
const obj = this.queue[0]
|
||||||
|
await obj.fn()
|
||||||
|
this._remove(obj)
|
||||||
|
if (!this.destroyed && this.queue.length) this._next()
|
||||||
|
}
|
||||||
|
|
||||||
|
_remove (obj) {
|
||||||
|
if (!this.destroyed) this.queue.splice(this.queue.indexOf(obj), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
this.destroyed = true
|
||||||
|
this.queue = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
249
app/js/webseed.js
Normal file
249
app/js/webseed.js
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
/* global gsignin */
|
||||||
|
import BitField from 'bitfield'
|
||||||
|
import sha1 from 'simple-sha1'
|
||||||
|
import Wire from 'bittorrent-protocol'
|
||||||
|
import { load, client, auth2 } from 'gapi'
|
||||||
|
import { keys } from './keys.js'
|
||||||
|
import { concat, sleep, Queue } from './util.js'
|
||||||
|
import halfmoon from 'halfmoon'
|
||||||
|
|
||||||
|
// i'll fucking give you 'thenable', shitass callbacks
|
||||||
|
export const token = new Promise(resolve => {
|
||||||
|
load('client:auth2', () => {
|
||||||
|
client.init(Object.assign({}, keys)).then(() => {
|
||||||
|
const auth = auth2.getAuthInstance()
|
||||||
|
auth.isSignedIn.listen(state => {
|
||||||
|
if (state === true) {
|
||||||
|
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
|
||||||
|
halfmoon.initStickyAlert({
|
||||||
|
content: 'You didn\'t grant permission for Google Drive access, try again!',
|
||||||
|
title: 'Login Error',
|
||||||
|
alertType: 'alert-danger',
|
||||||
|
fillType: ''
|
||||||
|
})
|
||||||
|
return auth.signOut()
|
||||||
|
}
|
||||||
|
resolve(auth.currentUser.get().getAuthResponse().access_token)
|
||||||
|
gsignin.classList.add('d-none')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!auth.isSignedIn.get()) {
|
||||||
|
gsignin.onclick = auth.signIn
|
||||||
|
gsignin.classList.remove('d-none')
|
||||||
|
} else {
|
||||||
|
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
|
||||||
|
halfmoon.initStickyAlert({
|
||||||
|
content: 'You didn\'t grant permission for Google Drive access, try again!',
|
||||||
|
title: 'Login Error',
|
||||||
|
alertType: 'alert-danger',
|
||||||
|
fillType: ''
|
||||||
|
})
|
||||||
|
return auth.signOut()
|
||||||
|
}
|
||||||
|
resolve(auth.currentUser.get().getAuthResponse().access_token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function getFileNameID (name) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
token.then(() => {
|
||||||
|
client.drive.files.list({
|
||||||
|
pageSize: 1,
|
||||||
|
fields: 'files(id)',
|
||||||
|
includeItemsFromAllDrives: true,
|
||||||
|
supportsAllDrives: true,
|
||||||
|
corpora: 'allDrives',
|
||||||
|
q: `name = '${name}'`
|
||||||
|
}).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
resolve(res?.result?.files?.length && res?.result?.files[0].id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export async function GDHandleTorrent (torrent) {
|
||||||
|
await token
|
||||||
|
for await (const file of torrent.files) {
|
||||||
|
const gDriveID = await getFileNameID(file.name)
|
||||||
|
if (gDriveID) {
|
||||||
|
file.gDriveID = gDriveID
|
||||||
|
} else {
|
||||||
|
halfmoon.initStickyAlert({
|
||||||
|
content: `Couldn't find a GDrive file for ${file.name}! Can't create webseed!`,
|
||||||
|
title: 'GDrive Error!',
|
||||||
|
alertType: 'alert-secondary',
|
||||||
|
fillType: ''
|
||||||
|
})
|
||||||
|
return null // if a single entry doesnt exist, I can't create a reliable webseed
|
||||||
|
}
|
||||||
|
await sleep(1000)
|
||||||
|
}
|
||||||
|
torrent.addWebSeed(new WebConn(torrent))
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebConn extends Wire {
|
||||||
|
constructor (torrent) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.connId = torrent.infoHash + ' gdrive' // Unique id to deduplicate web seeds
|
||||||
|
this.webPeerId = sha1.sync(this.connId)
|
||||||
|
this._torrent = torrent
|
||||||
|
this.lastRequest = {}
|
||||||
|
|
||||||
|
this.sleep = null
|
||||||
|
|
||||||
|
this.queue = new Queue()
|
||||||
|
|
||||||
|
this._init()
|
||||||
|
}
|
||||||
|
|
||||||
|
_init () {
|
||||||
|
this.setKeepAlive(true)
|
||||||
|
|
||||||
|
this.once('handshake', (infoHash, peerId) => {
|
||||||
|
if (this.destroyed) return
|
||||||
|
this.handshake(infoHash, this.webPeerId)
|
||||||
|
const numPieces = this._torrent.pieces.length
|
||||||
|
const bitfield = new BitField(numPieces)
|
||||||
|
for (let i = 0; i <= numPieces; i++) {
|
||||||
|
bitfield.set(i, true)
|
||||||
|
}
|
||||||
|
this.bitfield(bitfield)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.once('interested', () => {
|
||||||
|
this.unchoke()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.on('request', (pieceIndex, offset, length, callback) => {
|
||||||
|
const request = () => this.queue.add({
|
||||||
|
fn: async () => await this.httpRequest(pieceIndex, offset, length, (err, data) => {
|
||||||
|
if (err || data?.length !== length) return request()
|
||||||
|
callback(err, data)
|
||||||
|
}),
|
||||||
|
index: pieceIndex
|
||||||
|
})
|
||||||
|
request()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async httpRequest (pieceIndex, offset, length, cb) {
|
||||||
|
const pieceOffset = pieceIndex * this._torrent.pieceLength
|
||||||
|
const rangeStart = pieceOffset + offset
|
||||||
|
const rangeEnd = rangeStart + length - 1
|
||||||
|
const files = this._torrent.files
|
||||||
|
let requests
|
||||||
|
if (files.length <= 1) {
|
||||||
|
requests = [{
|
||||||
|
url: `https://www.googleapis.com/drive/v3/files/${files[0].gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`,
|
||||||
|
start: rangeStart,
|
||||||
|
end: rangeEnd
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
const requestedFiles = files.filter(file => file.offset <= rangeEnd && (file.offset + file.length) > rangeStart)
|
||||||
|
if (requestedFiles.length < 1) {
|
||||||
|
return cb(new Error('Could not find file corresponding to web seed range request'))
|
||||||
|
}
|
||||||
|
|
||||||
|
requests = requestedFiles.map(file => {
|
||||||
|
const fileEnd = file.offset + file.length - 1
|
||||||
|
const url = `https://www.googleapis.com/drive/v3/files/${file.gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
fileOffsetInRange: Math.max(file.offset - rangeStart, 0),
|
||||||
|
start: Math.max(rangeStart - file.offset, 0),
|
||||||
|
end: Math.min(fileEnd, rangeEnd - file.offset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let numRequestsSucceeded = 0
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
let ret
|
||||||
|
if (requests.length > 1) {
|
||||||
|
ret = Buffer.alloc(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
let { res, reader, endRange, setSize, ctrl } = this.lastRequest
|
||||||
|
for await (const request of requests) {
|
||||||
|
const { url, start, end } = request
|
||||||
|
function onResponse (res, data) {
|
||||||
|
if (!res || res.status < 200 || res.status >= 300) {
|
||||||
|
if (hasError) return
|
||||||
|
hasError = true
|
||||||
|
return cb(new Error(`Unexpected HTTP status code ${res?.status}`))
|
||||||
|
}
|
||||||
|
if (requests.length === 1) {
|
||||||
|
cb(null, data)
|
||||||
|
} else {
|
||||||
|
data.copy(ret, request.fileOffsetInRange)
|
||||||
|
if (++numRequestsSucceeded === requests.length) {
|
||||||
|
cb(null, ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (endRange !== start - 1 || ctrl?.signal?.aborted) {
|
||||||
|
async function * read (reader) { // <3 Endless
|
||||||
|
let buffered = []
|
||||||
|
let bufferedBytes = 0
|
||||||
|
let done = false
|
||||||
|
let size = 512
|
||||||
|
const setSize = x => { size = x }
|
||||||
|
yield setSize
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const it = await reader.read()
|
||||||
|
done = it.done
|
||||||
|
if (done) {
|
||||||
|
yield concat(buffered, bufferedBytes)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
bufferedBytes += it.value.byteLength
|
||||||
|
buffered.push(it.value)
|
||||||
|
|
||||||
|
while (bufferedBytes >= size) {
|
||||||
|
const b = concat(buffered)
|
||||||
|
bufferedBytes -= size
|
||||||
|
yield b.slice(0, size)
|
||||||
|
buffered = [b.slice(size, b.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctrl) ctrl.abort()
|
||||||
|
ctrl = new AbortController()
|
||||||
|
await this.sleep
|
||||||
|
try {
|
||||||
|
res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
range: `bytes=${start}-`,
|
||||||
|
authorization: 'Bearer ' + await token
|
||||||
|
},
|
||||||
|
signal: ctrl.signal
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.sleep = sleep(500)
|
||||||
|
onResponse()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
this.sleep = sleep(500)
|
||||||
|
reader = read(res.body.getReader(), 1)
|
||||||
|
setSize = (await reader.next()).value // lazy, but 1st yield is callback x)
|
||||||
|
}
|
||||||
|
endRange = end
|
||||||
|
setSize(end - start + 1)
|
||||||
|
onResponse(res, (await reader.next()).value || new Uint8Array())
|
||||||
|
}
|
||||||
|
this.lastRequest = { res, reader, endRange, setSize, ctrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
this.lastRequest?.ctrl?.abort()
|
||||||
|
this.queue.destroy()
|
||||||
|
this.lastRequest?.ctrl?.abort()
|
||||||
|
super.destroy()
|
||||||
|
this._torrent = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ function addConnection (connection) {
|
||||||
peer = new window.Peer({ polite: false })
|
peer = new window.Peer({ polite: false })
|
||||||
|
|
||||||
peer.pc.ontrack = evt => {
|
peer.pc.ontrack = evt => {
|
||||||
evt.receiver.playoutDelayHint = 3
|
evt.receiver.playoutDelayHint = 0.5
|
||||||
if (!video.srcObject) {
|
if (!video.srcObject) {
|
||||||
video.srcObject = evt.streams[0]
|
video.srcObject = evt.streams[0]
|
||||||
video.volume = 1
|
video.volume = 1
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta property="og:title" content="Miru">
|
<meta property="og:title" content="Miru">
|
||||||
<meta property="og:url" content="https://mirumoe.netlify.app/">
|
<meta property="og:url" content="https://miru.pages.dev/">
|
||||||
<meta property="og:description" content="Miru - Torrent streaming made simple!">
|
<meta property="og:description" content="Miru - Torrent streaming made simple!">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<meta property="og:image" content="logo.png">
|
<meta property="og:image" content="logo.png">
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
"entertainment"
|
"entertainment"
|
||||||
],
|
],
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"orientation": "landscape",
|
|
||||||
"background_color": "#191c20",
|
"background_color": "#191c20",
|
||||||
"theme_color": "#191c20",
|
"theme_color": "#191c20",
|
||||||
"scope": "/app/",
|
"scope": "/app/",
|
||||||
|
|
@ -28,14 +27,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"intent_filters": {
|
"intent_filters": {
|
||||||
"scope_url_scheme": "http",
|
"scope_url_scheme": "https",
|
||||||
"scope_url_host": "localhost",
|
"scope_url_host": "miru.pages.dev",
|
||||||
"scope_url_path": "/app/"
|
"scope_url_path": "/app/"
|
||||||
},
|
},
|
||||||
"capture_links": "existing-client-navigate",
|
"capture_links": "existing-client-navigate",
|
||||||
"url_handlers": [
|
"url_handlers": [
|
||||||
{
|
{
|
||||||
"origin": "https://localhost:5500"
|
"origin": "https://miru.pages.dev"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"protocol_handlers": [
|
"protocol_handlers": [
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ module.exports = {
|
||||||
externals: {
|
externals: {
|
||||||
halfmoon: 'halfmoon',
|
halfmoon: 'halfmoon',
|
||||||
anitomyscript: 'anitomyscript',
|
anitomyscript: 'anitomyscript',
|
||||||
gapi: 'commonjs gapi'
|
gapi: 'globalThis.gapi'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: path.resolve(__dirname, 'app/js'),
|
path: path.resolve(__dirname, 'app/js'),
|
||||||
sourceMapFilename: 'bundle.js.map'
|
sourceMapFilename: 'bundle.js.map'
|
||||||
},
|
},
|
||||||
mode: 'development',
|
mode: 'production',
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue