mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 02:21:49 +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)
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@ body {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
noscript {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: #000;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#gsignin {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.badge-color {
|
||||
background-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 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:url" content="">
|
||||
<meta property="og:url" content="https://miru.pages.dev/">
|
||||
<meta property="og:description" content="Miru - Torrent streaming made simple!">
|
||||
<meta property="og:type" content="video.other">
|
||||
<meta property="og:image" content="logo.png">
|
||||
|
|
@ -26,6 +26,16 @@
|
|||
</head>
|
||||
|
||||
<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="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">
|
||||
|
|
@ -155,16 +165,16 @@
|
|||
<h1 class="title font-weight-bold text-white">Sypnosis</h1>
|
||||
<div class="font-size-16 pr-15">
|
||||
</div>
|
||||
<h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
|
||||
<div class="d-flex flex-wrap justify-content-start">
|
||||
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
|
||||
<!-- <h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
|
||||
<div class="d-flex flex-wrap justify-content-start"> -->
|
||||
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
|
||||
<img loading="lazy"
|
||||
src="https://img1.ak.crunchyroll.com/i/spire1-tmb/b199406edeebc19a7f4e4412d6e1dfcc1365964779_full.jpg"
|
||||
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
|
||||
Years in the Future -The Fall of Zhiganshina (1)</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
<div class="col-md-3 px-sm-0 px-20">
|
||||
<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"
|
||||
autocomplete="off" value="SubsPlease">
|
||||
<datalist id="subtitle1list">
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<label for="player10">Pause When Tabbing Out</label>
|
||||
</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"
|
||||
autocomplete="off" value="SubsPlease">
|
||||
<datalist id="torrent4list">
|
||||
<option value="SubsPlease">https://subsplease.org/rss/?r=</option>
|
||||
<option value="Erai-raws">https://www.erai-raws.info/rss-</option>
|
||||
<option value="SubsPlease">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=subsplease&q=</option>
|
||||
<option value="Erai-raws">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=Erai-raws&q=</option>
|
||||
</datalist>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<select class="form-control form-control-lg" id="torrent1">
|
||||
<option value="1080" selected>1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value='480"||"540'>SD</option>
|
||||
<option value='"1080"' selected>1080p</option>
|
||||
<option value='"720"'>720p</option>
|
||||
<option value='"480"||"540"'>SD</option>
|
||||
</select>
|
||||
</div>
|
||||
<break></break>
|
||||
|
|
@ -998,6 +1004,11 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
|
|||
<input type="checkbox" id="torrent9">
|
||||
<label for="torrent9">Batch Lookup</label>
|
||||
</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 class="content my-10">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { alID } from './interface.js'
|
||||
import { settings } from './settings.js'
|
||||
import halfmoon from 'halfmoon'
|
||||
|
||||
async function handleRequest (opts) {
|
||||
|
|
@ -71,7 +72,8 @@ export async function alRequest (opts) {
|
|||
perPage: opts.perPage || 30,
|
||||
status_in: opts.status_in || '[CURRENT,PLANNING]',
|
||||
chunk: opts.chunk || 1,
|
||||
perchunk: opts.perChunk || 30
|
||||
perchunk: opts.perChunk || 30,
|
||||
startDate: (!settings.other3 && (opts.startDate || 20210328)) || 10000000
|
||||
}
|
||||
const options = {
|
||||
method: 'POST',
|
||||
|
|
@ -260,12 +262,12 @@ query ($page: Int, $perPage: Int, $from: Int, $to: Int) {
|
|||
variables.status = opts.status
|
||||
variables.sort = opts.sort || 'SEARCH_MATCH'
|
||||
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) {
|
||||
pageInfo {
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,14 +111,8 @@ export async function resolveFileMedia (opts) {
|
|||
async function resolveTitle (title) {
|
||||
if (!(title in relations)) {
|
||||
// resolve name and shit
|
||||
let method, res
|
||||
if (opts.isRelease) {
|
||||
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)
|
||||
const method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH', startDate: 10000000 }
|
||||
let res = await alRequest(method)
|
||||
if (!res.data.Page.media[0]) {
|
||||
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('-', '')
|
||||
|
|
@ -138,7 +132,11 @@ export async function resolveFileMedia (opts) {
|
|||
const parseObjs = await Promise.all(parsePromises)
|
||||
await Promise.all([...new Set(parseObjs.map(obj => obj.anime_title))].map(title => resolveTitle(title)))
|
||||
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 = []
|
||||
for (const praseObj of parseObjs) {
|
||||
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
|
||||
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
|
||||
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 })
|
||||
} 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"
|
||||
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 {
|
||||
episode = opts.episode - (opts.offset + tempMedia.episodes)
|
||||
episode = opts.episode - (opts.offset + media.episodes)
|
||||
}
|
||||
if (opts.increment || increment) {
|
||||
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 { settings } from './settings.js'
|
||||
import { client } from './main.js'
|
||||
import { countdown, flattenObj } from './util.js'
|
||||
import { countdown, flattenObj, userBrowser } from './util.js'
|
||||
import halfmoon from 'halfmoon'
|
||||
export function loadHomePage () {
|
||||
const homeLoadElements = [navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore]
|
||||
|
|
@ -19,7 +19,7 @@ export function loadHomePage () {
|
|||
if (!page) gallerySkeleton(browseGallery)
|
||||
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 => {
|
||||
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 })
|
||||
return res.data.Page.pageInfo.hasNextPage
|
||||
|
|
@ -94,7 +94,7 @@ export function loadHomePage () {
|
|||
continue: function () {
|
||||
alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, perPage: 50 }).then(res => {
|
||||
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)
|
||||
galleryAppend({ media: mediaList, gallery: homeContinue })
|
||||
})
|
||||
|
|
@ -305,6 +305,7 @@ function genreBadges (genres = []) {
|
|||
return badges
|
||||
}
|
||||
const detailsMap = [
|
||||
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
||||
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
||||
{ property: 'season', label: 'Season', icon: 'spa', 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: 'english', label: 'English', icon: 'title' },
|
||||
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
||||
{ property: 'native', label: 'Native', icon: '日本', custom: 'icon' }
|
||||
{ property: 'native', label: 'Native', icon: '語', custom: 'icon' }
|
||||
]
|
||||
/* global detailTemplate */
|
||||
const detailTemp = detailTemplate.cloneNode(true).content
|
||||
|
|
@ -358,6 +359,8 @@ function mediaDetails (media) {
|
|||
nodes[4].textContent = media.averageScore + '%'
|
||||
} else if (detail.property === 'season') {
|
||||
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 {
|
||||
nodes[4].textContent = media[detail.property]
|
||||
}
|
||||
|
|
@ -425,8 +428,8 @@ export function viewMedia (input, episode) {
|
|||
|
||||
viewNodes[63].innerHTML = media.description
|
||||
|
||||
viewNodes[68].textContent = ''
|
||||
viewNodes[68].append(...mediaDetails(media))
|
||||
viewNodes[66].textContent = ''
|
||||
viewNodes[66].append(...mediaDetails(media))
|
||||
}
|
||||
|
||||
export let alID // login icon
|
||||
|
|
@ -454,3 +457,12 @@ export function initMenu () {
|
|||
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 { cardCreator, initMenu } from './interface.js'
|
||||
import halfmoon from 'halfmoon'
|
||||
import { GDHandleTorrent } from './webseed.js'
|
||||
|
||||
const playerControls = {}
|
||||
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]
|
||||
}
|
||||
}
|
||||
window.addEventListener('beforeunload', () => {
|
||||
client.destroy()
|
||||
})
|
||||
export const client = new WebTorrentPlayer({
|
||||
WebTorrentOpts: {
|
||||
maxConns: 127,
|
||||
|
|
@ -23,7 +27,8 @@ export const client = new WebTorrentPlayer({
|
|||
uploadLimit: settings.torrent7 * 1572864,
|
||||
tracker: {
|
||||
announce: settings.torrent10.split('\n')
|
||||
}
|
||||
},
|
||||
destroyStore: true
|
||||
},
|
||||
controls: playerControls,
|
||||
video: video,
|
||||
|
|
@ -48,6 +53,7 @@ client.on('download-done', ({ file }) => {
|
|||
fillType: ''
|
||||
})
|
||||
})
|
||||
client.on('torrent', GDHandleTorrent)
|
||||
client.on('watched', ({ filemedia }) => {
|
||||
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
|
||||
if (settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
|
||||
|
|
@ -135,8 +141,6 @@ client.nowPlaying = { name: 'Miru' }
|
|||
|
||||
window.client = client
|
||||
|
||||
window.onbeforeunload = () => { return '' }
|
||||
|
||||
if (searchParams.get('file')) client.playTorrent(searchParams.get('file'))
|
||||
|
||||
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 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 quality = `"${settings.torrent1}"` || '"1080p"'
|
||||
const quality = `${settings.torrent1}` || '"1080p"'
|
||||
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})`)
|
||||
|
||||
|
|
@ -101,8 +101,8 @@ export async function nyaaRss (media, episode, isOffline) {
|
|||
|
||||
export function getRSSurl () {
|
||||
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 {
|
||||
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'
|
||||
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)
|
||||
settingsTab.addEventListener('click', applySettingsTimeout)
|
||||
|
|
@ -39,10 +39,6 @@ async function saveSettings () {
|
|||
}
|
||||
}
|
||||
|
||||
if (Object.keys(settings).length !== settingsElements.length + 1) {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
let applyTimeout
|
||||
function applySettingsTimeout () {
|
||||
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]
|
||||
}
|
||||
|
||||
saveSettings()
|
||||
|
||||
other1.oninput = () => other1.checked && Notification.requestPermission().then(perm => { perm === 'denied' ? other1.checked = false : other1.checked = true })
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
import halfmoon from 'halfmoon'
|
||||
|
||||
halfmoon.showModal = id => {
|
||||
const t = document.getElementById(id)
|
||||
t && t.classList.add('show')
|
||||
}
|
||||
|
||||
halfmoon.hideModal = id => {
|
||||
const t = document.getElementById(id)
|
||||
t && t.classList.remove('show')
|
||||
}
|
||||
|
||||
export const searchParams = new URLSearchParams(location.href)
|
||||
if (searchParams.get('access_token')) {
|
||||
localStorage.setItem('ALtoken', searchParams.get('access_token'))
|
||||
window.location = '/app/#home'
|
||||
}
|
||||
|
||||
export const userBrowser = (() => {
|
||||
if (window.chrome) {
|
||||
return (navigator.userAgent.indexOf('Edg') !== -1) ? 'edge' : 'chromium'
|
||||
}
|
||||
return 'firefox'
|
||||
})()
|
||||
|
||||
export function countdown (s) {
|
||||
const d = Math.floor(s / (3600 * 24))
|
||||
s -= d * 3600 * 24
|
||||
|
|
@ -50,3 +55,67 @@ export function flattenObj (obj) {
|
|||
}
|
||||
|
||||
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.pc.ontrack = evt => {
|
||||
evt.receiver.playoutDelayHint = 3
|
||||
evt.receiver.playoutDelayHint = 0.5
|
||||
if (!video.srcObject) {
|
||||
video.srcObject = evt.streams[0]
|
||||
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 name="viewport" content="width=device-width" />
|
||||
<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:type" content="video.other">
|
||||
<meta property="og:image" content="logo.png">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"entertainment"
|
||||
],
|
||||
"lang": "en-US",
|
||||
"orientation": "landscape",
|
||||
"background_color": "#191c20",
|
||||
"theme_color": "#191c20",
|
||||
"scope": "/app/",
|
||||
|
|
@ -28,14 +27,14 @@
|
|||
}
|
||||
},
|
||||
"intent_filters": {
|
||||
"scope_url_scheme": "http",
|
||||
"scope_url_host": "localhost",
|
||||
"scope_url_scheme": "https",
|
||||
"scope_url_host": "miru.pages.dev",
|
||||
"scope_url_path": "/app/"
|
||||
},
|
||||
"capture_links": "existing-client-navigate",
|
||||
"url_handlers": [
|
||||
{
|
||||
"origin": "https://localhost:5500"
|
||||
"origin": "https://miru.pages.dev"
|
||||
}
|
||||
],
|
||||
"protocol_handlers": [
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ module.exports = {
|
|||
externals: {
|
||||
halfmoon: 'halfmoon',
|
||||
anitomyscript: 'anitomyscript',
|
||||
gapi: 'commonjs gapi'
|
||||
gapi: 'globalThis.gapi'
|
||||
},
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'app/js'),
|
||||
sourceMapFilename: 'bundle.js.map'
|
||||
},
|
||||
mode: 'development',
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
|
|
|
|||
Loading…
Reference in a new issue